Add Contact Center module - #501
MikeAlhayek wants to merge 635 commits into
Conversation
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
|
@copilot resolve the merge conflicts in this pull request |
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
A Web Audio source node binds to the track it was built from and does not follow a track swapped into the same stream. So after a mid-call microphone or boost change the probes went on measuring the old, now stopped, track and reported silence for the rest of the call: on a live call the encoder's own measurement read full scale while the probe beside it read 0.000. Telemetry that contradicts itself is worse than none, because it invites chasing the wrong half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Half of the silence an agent waits through between finishing a sentence and hearing the reply is spent inside their own browser. Chrome holds arriving audio before playing it, sized for the jitter it expects rather than the jitter it is seeing: on these calls the network jitter measured 3-13 ms while the buffer held 60-105 ms, and it opened at 310-628 ms for the first half-minute of a call, which is why the beginning of a call feels worst. All of it sits in the return leg of a conversation. An Audio delay setting asks the browser for a shorter hold, applied to the call already in progress. It is a hint, not a command: the browser still grows the buffer when packets really do arrive late, so this trades a smaller safety margin rather than forcing anything, and Automatic -- no hint at all, exactly as before -- remains the default. The shortest offered hold is deliberately not zero, because a buffer of nothing conceals on the first packet that is a millisecond late and sounds worse than the delay it removes. What that costs is now measured beside it. Concealment -- the share of received audio the browser had to invent because its packet arrived too late to play -- is reported per sample, to the diagnostics readout and the server log. A shorter buffer is an improvement only while concealment stays near zero, and the two numbers have to be read together; the payload contract test caught the field before it could reach a report that had nowhere to bind it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Most of what this cost was learning what to measure: six confident explanations were each overturned by a measurement, and the instrumentation is the part worth keeping. The page records how to read a quality sample when a caller complains, the defects that reading it exposed, the settings an agent can change on a live call, and the limits no setting removes -- the G.711 bandwidth to a PSTN caller, and the delay a browser bridged to the telephone network will always carry. It ends with the caution the investigation earned: recorded prompts reach the same caller over the same leg sounding clear, which refuted the conclusion that bandwidth explained "muffled". When a structural limit and a fixable one both explain a symptom, prefer the one that can be measured, and measure it before deciding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The provider decides which of its points of presence a browser connects to by looking up where that browser appears to be, and says in its own documentation that the answer can be wrong -- their example is a client in India routed to Frankfurt rather than Chennai, with the call latency that implies. An agent inherited whatever that lookup returned, nothing in the soft phone ever said which location they had landed on, and there was no way to move. A tenant can now pin the location, and an agent can pick a nearer one for themselves under the gear icon. The agent's choice has to be able to win: the right edge follows where the person is sitting, so a team split across two continents has no single right answer, and the tenant setting is a starting point for everyone rather than a rule over anyone. Automatic falls through to the tenant, and then to the provider's routing, so nothing changes for anyone who sets nothing. A location the provider does not recognize is dropped rather than passed on, on both sides of the wire. The SDK turns the value into a signaling hostname, so an unrecognized one resolves to nothing and the agent cannot register at all -- worse than the routing the setting exists to correct. Unlike every other audio setting this one is fixed when the provider client is built, so changing it rebuilds the registration. That runs through the path that already revokes the credential it replaces, because an agent may only hold so many before the provider refuses the login; under a live call it waits, since rebuilding would drop the call. Scope, because it is easy to assume more: this moves the SIGNALING edge. The provider documents signaling and media as separate planes and does not say how a browser leg's media gateway is chosen, so whether media follows is a question for the round-trip figure on a call to answer, not for this commit to claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An automated call is classified by a model reading its transcript, and the choice it makes is not a label: a disposition is what the subject's workflow acts on next. The rule was already right -- offer the dispositions the subject's own actions are wired to, fall back to the whole catalog for a subject nobody has wired up yet, and refuse a value that was not on offer -- but it lived inline in the conclusion, where the only thing that had ever proved it was a single live phone call. The rules move to the conclusion policy beside the ones about silent calls and escalated ones, which is where a reader looks for what a finished call may claim happened on it, and where they can be exercised without a provider account. Nine cases cover them, including the two that matter most and were previously unproven: a disposition the model invented is discarded, and so is one that exists in the catalog but belongs to a different subject -- a call concluded that way would put an outcome on the record that nothing downstream can act on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things a live automated call could not do, both for the same reason. A realtime call is a speech-to-speech session: the model is speaking, not returning text, so the hangup marker the turn-based loop writes into its reply has nowhere to go. The assistant said goodbye and then held the line until the customer worked out that nobody was going to hang up. Observed on a live call -- the closing line, and then an open line until the customer disconnected. The model now has a tool to end the call with, and the session does the hanging up. Three things have to be true first, and each is somebody's experience of the call: the model has decided it is over, its closing line has actually finished playing, and the customer has had a breath to answer it. If they use that breath -- "actually, one more thing", which is when people say it -- the hangup is abandoned rather than deferred, and the assistant answers them. A customer who is still talking has not finished the call, whatever the model concluded. The grace runs from the end of the goodbye rather than from the tool call, so a long closing line does not eat the window the customer was supposed to get; and the session waits for the goodbye to START, because the tool lands first and the silence before the first word is not the end of anything. Escalation was broken by the same gap, silently. A realtime session is configured once, from the profile, and never sees the per-turn tool wiring the turn-based path does -- so the transfer tool was never attached and the guidance never reached the model. The loop read a handoff flag that nothing on a realtime call could set: a caller asking for a person was talked to by the assistant instead. Both tools now go in where a realtime session is built, and the transfer tool only where the call actually has a queue behind it, because offering it otherwise promises a person who is not coming. Checked against the unfixed code first: the closing tests fail there. One hung rather than failing, which is why they are bounded -- a regression should fail, not stall the suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A caller asked for a person. The model said "connecting you with an agent now", the tool recorded the transfer, the assistant went quiet -- and nobody was ever enqueued. The caller sat on an open line in silence, with no hold music, because hold music belongs to a queue they were never put in. Not one line was logged. A realtime session holds the caller for the whole call, and it does so inside the provider's "call answered" webhook request. No provider waits minutes for a webhook response: ours timed out after seconds, retried the webhook twice, and redelivered it again seven minutes later. When the session finally ended, the application aborted a connection that had been dead for a minute and a half, and the handoff that came next went with it -- along with the request's cancellation token, already cancelled, which would have failed the enqueue before it could log anything anyway. So the call is now finished somewhere else: a child scope, with its own services, its own clock on the work, and a cancellation token of its own. What the session decided travels as plain values, because nothing from the dying scope can be carried across it. The tests that let this through were the interesting part. One asserted that the loop enqueues when the handoff flag is set, and stayed green throughout -- while the live path could neither set that flag nor survive long enough to act on it. It tested the branch and not the journey. The replacements cross the seam: one proves the finishing work is handed on when the request is already aborted, the other proves that work enqueues the caller with a token nobody has cancelled. Both fail against the previous shape. The loop passed the file-size line on the way, so the handoff and the ending of a call now live in a partial of their own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The transfer still did not reach an agent. The tool recorded it, the assistant went quiet, the connection was aborted four seconds later, and the queue stayed empty -- with nothing logged between the tool and the abort, which is what made the last two attempts guesswork. The child scope was still the request's child, so it died with the request. The scope now comes from the shell host, which belongs to the tenant rather than to any request, and the work is not awaited by the handler that started it. That is the shape the SMS webhook endpoint already uses, for the same reason written on it there: a provider gives a webhook seconds, and a voice session holds one for the length of a call. Silence is no longer a possible outcome. The session ending, the decision it reached, the attempt to act on it and the work itself each say so, so if this still does not reach an agent the log will say where it stopped rather than leaving it to be inferred from a Kestrel connection message. Also, from the same call: the workspace listed +17024445506 where a person expects (702) 444-5506, and never showed how long a call lasted. The formatter was there but only in the soft phone's bundle, and the workspace has no soft phone on the page -- so it silently fell back to raw digits. It is shared with the telephony client now, with a bundle test holding it there, and history shows talk time from answer to end. A call that was never answered shows none, because "0:00" reads as a call that connected and said nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…never comes
The closing watchdog waited on the end-call signal and nothing else:
await Task.Delay(Timeout.InfiniteTimeSpan, endCallRequested);
On a transfer that signal never arrives. The model asked for a person, not for
the call to end -- so the watchdog never finished, and the teardown waits for
every pump before the session returns. The session therefore never returned, and
everything written after it was unreachable: the handoff, the enqueue, the hold
music, the offer. A caller was told a person was coming and then left on a line
nobody could be seated on, and the log fell silent at exactly the moment the
work should have started, because the code that logs it was never reached.
The same was true of the commonest ending of all -- the caller simply hanging up
-- which is why calls that ended that way wrote no outcome either.
The watchdog now watches the call scope alongside the signal, so it ends when
the call does, whatever ended it.
Three live tests were spent looking for this in the wrong place. Each time the
absence of a log line was read as "the work ran and failed quietly" when it
meant "the work never started"; the instrumentation added along the way is what
finally made the difference between those two readable.
Every existing test passed straight through this defect, because none of them
supplied an end-call token at all -- with no token the watchdog never starts.
The two added here do, one for a transferred call and one for a caller who hung
up, and both hang and then fail against the old wait.
Verified live end to end: transfer recorded, session ended, caller enqueued and
reserved, hold music played, offer accepted, both legs bridged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deleting a voicemail answered 404, and so did playing one -- the same 404, from the same cause, which is why the inbox looked fine and neither button did anything. The soft phone addresses a voicemail by the identifier on its own inbox row. That is the platform interaction's identifier only when the platform projected the row; a call the soft phone sent to voicemail itself carries a generated one. Both endpoints looked it up as an interaction identifier, found nothing, and said "not found" about a recording sitting in plain sight in the agent's inbox. The database says it plainly: for the voicemail on this system, a lookup by interaction identifier matches nothing and a lookup by the provider call it was recorded on matches exactly one. The inbox row knows that call, so the endpoints fall back to it -- reading the row as the signed-in user's own, so an identifier belonging to somebody else resolves to nothing rather than to their voicemail. Two things found while fixing it. The playback audit and the deletion erase were both keyed to whatever identifier the soft phone asked with, so on these rows an audit trail would have been written against something that is not the recording it is about; both now use the interaction that was resolved. And a test written for the fallback failed until that was corrected, which is how it surfaced. Also included, from the live transfer testing: the queue work that makes an offered caller hear something (an offered caller is Reserved, and treatment passes read Waiting only, so no pass would ever play to them), and the handoff work that goes with it. Verified live end to end -- hold music played, the offer was accepted, both legs bridged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main carries the platform from preview.247 to preview.270, which drops AIDeploymentPurpose and, with it, the profile's own realtime deployment. Realtime is a capability a model either has or does not now, so the question this branch asked -- "was a second deployment configured" -- has no answer any more. It asks the one that replaced it: can the deployment this profile already chats on hold a live conversation. Four conflicts. Two were using blocks and took main's side. The changelog keeps both entries. The SMS handler had diverged furthest -- this branch has the handoff, the conversation gate, the lock and the humanized pacing on it -- so it keeps this branch's version with main's slot resolution applied, rather than either side wholesale. The version bump then reaches past the conflicts: five deployment resolutions move to the slot API, the live-session decision moves to the capability service, and the deployment a session is held on travels on the call context instead of being read off the profile. The whole suite passes, and the tests that cover the agent handoff were written against live failures rather than around them. The live-session decision itself is the one thing worth a phone before it is trusted: it is new code on the exact path four live calls were spent on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four live calls went into one agent handoff, and each time the suite looked healthy because every test stopped at a seam. That is worth writing down once, next to the list of what each remaining live test is actually for -- so a call is spent on a question only a call can answer. The plan states the rule the last two days earned: a defect found live gets a test that fails against the code that produced it, before the fix is committed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The call opened "Hey, is this Marcus?" to a contact named Amani. Amani asked who it was looking for -- the only sensible reply -- and the assistant read that as a request for a human and transferred the call. The conversation was over before it began, and the compliance behaviour it was placed to test never came up. The turn-based greeting is rendered from a template with the contact in scope, so it opens with the real name. A live session is configured from the profile and never sees that template, so the model was told nothing about who answered. Asked to open a sales call with no name, a model does not decline to use one: it invents a plausible one, and the person who picks up knows at once that nobody actually knows them. The session is now told who it is calling, and told that it is the only name it may use. When there is no name, it is told to use none -- silence on the subject is what produced the invented one. The transfer guidance is tightened for the same call: a question is not a request for a person. "Who is this?", "who are you looking for?" and their like are asking the assistant to explain itself, and when it is unsure it should ask whether to put them through rather than transferring on a guess. Also completes the merge migration: the deployment a live session is held on is now actually set on the call context. The edit that was meant to set it silently matched nothing after the file moved, and the session only worked because the orchestrator falls back to a default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ho hangs up Three things this call exposed, and one that it could not. A caller asked not to be called and was put through to a queue instead. Speech recognition on a phone line drops small words: "don't call me" reached the model as "call me", which reads as a request to be connected and was acted on as one. The negation cannot be recovered after the fact, so the session is now told how this arrives -- that a short or garbled phrase around a refusal is an opt-out unless the customer clearly says otherwise, that an opt-out ends the call, and that somebody trying to end contact is never transferred. When it genuinely cannot tell, it asks rather than assuming the reading that keeps somebody on the list. The disposition for that call came back as an ordinary completed call. Choosing one is the most consequential thing the review does, because each disposition is wired to what happens to the customer next, and an opt-out recorded as a completed call is the single mistake here that cannot be undone later. The review is told so, and told to read every disposition it is given rather than the one that sounds positive. A caller who hung up while waiting for an agent stayed in the queue. The leg they were on belongs to the automated voice module, which deliberately keeps its events away from Contact Center routing -- right until the handover, and wrong immediately after it. The queue now hears about it through an abstraction the voice module can see, resolved optionally so automated voice still runs on a tenant with no Contact Center. Ending the interaction is what does the work: the reconciliation that cancels reservations and clears the queue only acts on an ended interaction, and the abandonment figure counts calls that ended without being answered -- so the number meant to show callers giving up was reading zero while exactly that happened. The conclusion prompt moves to Templates/Prompts as a file, like every other system prompt here, so it can be read and changed without editing C#. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…g up A profile whose model declares the realtime capability was still running the turn-based speak-and-transcribe loop. The deployment was resolved through the chat slot, and the framework excludes realtime from that slot on purpose -- a speech-to-speech model cannot serve a text completion -- so the lookup answered "no such deployment" for precisely the deployment being looked for. Nothing failed: the call connected, the assistant spoke, and the only symptom was on the phone, where it could not hear the caller while it was talking. It is now asked of the deployment catalog by capability, which is how the rest of the platform asks the same question, and a call that does fall back says so in the log rather than leaving the difference audible but untraced. The tests did not catch it because they set the chat slot up to hand back a realtime deployment, which the framework will not do. The harness now models the real behaviour, and eleven realtime tests fail against the old resolution. That fallback also had no way to end a call: the end-call tool reached realtime sessions only, so on a turn-based call the assistant said goodbye and then sat there until the customer hung up. It is now offered on every turn, alongside the transfer tool when the call has somewhere to transfer to, and the closing line is spoken in full before the line is cut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ll to hang up Resolving the realtime deployment correctly got the session as far as being started, where the orchestrator refused it: a session carrying tools needs an active AIInvocationScope, because that is where a tool reads its context from. The runner never opened one, so the refusal threw, the runner reported that it had held no session, and the call fell back to the turn-based loop -- the same symptom as before, one layer further in. The scope is now opened before the session and held for the whole call. The fake orchestrator in the tests accepted a session without one, which is why nothing failed here. It now refuses to answer that question falsely: it records what scope was current, and the test fails against the unfixed runner. Separately, only realtime sessions were ever told that hanging up is their job. A turn-based call had the tool attached and no idea it was expected to use it, so it said goodbye and waited for the customer to work out that nobody was going to hang up. That guidance now lives in one place and both paths give it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An automated call or message chooses its disposition from a list of names and descriptions, so that description is the whole of what the model has to go on. Until now it could only be the disposition's general description, and the same disposition legitimately means different things on different work: "Done" on a sales follow-up is not "Done" on a service call. A subject action can now carry its own wording, which replaces the general description for that subject's conversations. The editor starts the field off as a copy of the disposition's description when one is chosen, so writing the specific version is an edit rather than a blank page, and it only ever replaces text the author has not written themselves. Left empty, the general description is used exactly as before. Both channels ask the same question, so the rule that decides which description the model sees is written once and used by the voice conclusion and the SMS conclusion alike. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A disposition can set a contact's do-not-call, do-not-sms, do-not-email or do-not-chat preference. It was applied to the content item in memory and then dropped: no caller saved the contact, and the executor did not either. So a customer could ask not to be called, be dispositioned exactly right by the model, and be dialled again on the next inventory load -- which is what was happening on a contact that had been asked and recorded three times. The change and the write now live together, and the published version is updated when there is one, because the lists that decide who gets dialled or messaged query published contacts. A disposition that says nothing about contact preferences still writes nothing. The existing test asserted the preference on the in-memory item and stopped there, which is exactly the half of the behaviour that worked. It now asserts the save as well, and fails against the unfixed executor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Realtime voice and the assistant ending its own call are now proven on a live call rather than only by test. The compliance entry is narrowed to the half that never worked, and two new entries name what is held by test and has not yet been judged by a model on a phone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The window the caller gets after the assistant's goodbye was ended only by a transcript, and a transcript exists only once they have stopped talking and the provider has transcribed them -- seconds after they opened their mouth. So the person who said "actually, one more thing" was cut off mid-word by the very guard meant to protect them, and the test covering it passed because it queued a transcript rather than the speech that produces one. Voice detection fires on the first syllable, and that event was not handled at all. It now stamps the same clock, so the call stops being over the moment the caller decides it is not. The window itself goes from two seconds to four. Two was about the length of a breath and read on a real call as being hung up on; the cost of the extra couple of seconds is a little silence at the end of a call that was over anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things heard on a live call, both in the live session. The assistant waited to be spoken to. A turn begins with voice detection, and nothing ever asked the model to speak first, so every transcript opened with the customer saying "Hello?" into dead air on a call we placed. The session now asks for a response as soon as it is configured. And it said its goodbye twice, word for word. The model says the closing line, calls the end-call tool, reads the tool's reply and -- with the line still open while the customer is given their moment to answer -- produces the same line again. Nothing here can stop it being generated; what it can do is not play it, not count it towards the closing silence, and not write it to a transcript that is supposed to say what was said on the call. The customer speaking clears that, because then the call is not over after all. The pumps move to their own file: they are the part of the session that runs for the whole call, and the file had grown past the size the architecture guard allows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous attempt read the end-call request at the moment a line finished, which assumed the request arrives first. It does not: the model says its closing line and calls the tool after it, so the flag was set one line too late and the repeat was still played and still written to the transcript -- heard again on a live call. The request is now watched rather than read. The line in flight when it arrives is the goodbye and finishes; anything the model starts afterwards is the repeat. A request that arrives while nothing is being said means the goodbye is already behind us, so the next line is a repeat too. The test cancelled the token before queueing the goodbye, which is the easy ordering and the one that hid this. It now queues the line first and the request after, as a call does, and fails against the previous attempt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every conversation-level fault found on this platform so far -- a transfer nobody asked for, a goodbye said twice, a reply the model never heard -- was diagnosed from the stored transcript after the call, because the turns themselves left no trace at all. The realtime session logged four things, none of them a turn. Turns are now recorded at debug, and the two ways a caller can be lost are recorded at information, because neither is a detail: a transcript that comes back empty, and an utterance the provider could not transcribe. The second was not even handled -- it fell through the switch in silence, and it is the most likely explanation for an assistant that ignored a short "yes" and waited for it to be said again. Also records, as a test, that a session which answers by itself cannot be asked to open the call. The fake counted the request whatever kind of session it was, which is why asking it to speak first passed here while every live call still opened with the customer saying "Hello?" into silence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things a person on the phone noticed, and one they could not. The assistant never opened a call it had placed: a turn begins with voice detection, so the customer had to say "hello?" into silence before it would speak. It could not be fixed from here -- a session the provider answers for itself refused every way of creating a response -- so it was fixed in the framework and is used here now that it has shipped (CrestApps.Core #198, 2.0.0-preview.277). Nothing broke a silence either. The turn-based loop has always asked whether somebody is still there; the live session sat waiting, which is what a caller whose reply was lost sat through. It now asks after twelve quiet seconds, twice, and then stops -- a caller who has gone is not brought back by a third attempt. The wording is deliberate: asked only to check whether they were still there, the model filled the silence by moving the sale along, replacing a question the customer had missed with a new one. It is now told to ask that question or repeat the last one, and nothing else. Both clocks start at the call rather than at zero, or the first check measures a silence stretching back to the beginning of time and speaks over the opening line -- which the test caught before a caller did. The one nobody could see: the turns themselves left no trace. Every conversation-level fault found here was diagnosed from the stored transcript afterwards. They are now logged as they happen, and the two ways a caller can be lost -- an empty transcript, and an utterance that could not be transcribed -- are recorded plainly, the second having previously fallen through in silence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This pull request has merge conflicts. Please resolve those before requesting a review. |
A batch decides who to contact when it is loaded. What it creates is placed later -- hours later, longer if rescheduled -- and nothing looked at the contact again in between. Somebody who asked to be left alone in that window was still holding an activity with their number on it, and it was dialled. The question is now asked immediately before the call or message goes out, and the rule behind it lives in one place instead of being a few lines inside a destination lookup in one module, which is why three other paths never asked it at all: the SMS processor, which is the last code before the carrier and is also reached directly by the workflow task; the cadence that revives a quiet conversation, which would otherwise keep messaging somebody who had said stop, on a schedule, for days; and an automated voice call, which reached the provider without passing the boundary a person's call passes, so no national registry and no calling window applied to it. That one now goes through the same screening an agent's call does, as its own origin so a screener can tell the two apart. The batch's own "include do-not-calls" flags -- one per channel -- were stored, editable, cloned, API-baselined and read by nothing. They work now: unticked respects the preference, which is what the form has always implied, and ticking one is an operator deciding this particular message is not the kind a preference is meant to stop. Excluding opted-out contacts also no longer depends on the activity being automated; an agent's call sheet should not contain people who asked not to be called either. Two more defects the new tests pinned rather than papered over. The SMS conclusion handed a possibly-null disposition to an executor that rejects null, inside a deferred scope, after writing the outcome but before committing it -- so the conversation was left open with no disposition, no notes and no follow-up, and the only trace was a stack in a log. And the bulk-manage do-not-call filter compared a boolean column with the integer literal 1, which SQLite accepts and PostgreSQL refuses outright, while its inner join multiplied every activity by the number of index rows behind its contact -- the count shown before a bulk complete or purge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A contact could tick "do not chat", and it was stored, projected into an index, importable, exportable, settable by a subject action and shown in the editor -- and read by nothing. There is no chat channel on this platform: no constant, no processor, no path that creates chat work. Somebody who asked not to be chatted with was being told something the product had no way to keep, which is worse than not offering the choice. Its three siblings stay. Do-not-call and do-not-sms are enforced at four points each, and do-not-email gates the creation of email activities a person then works by hand -- a thin channel, but a real one. The recorded values are not destroyed: they stay verbatim in each contact's document and are ignored on deserialize, so nothing is unrecoverable if a chat channel ever arrives. Only the projection goes, and a projection is rebuildable. Both column drops are registered in the migration contract with the reason, rather than the guard being loosened to let them through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite was green all day on a Debug build, and the pipeline does not build that way: it builds Release with warnings as errors and analyzers on, and under those switches nine rules failed. Four were mine from today, four predate today and had been sitting as warnings nobody had to look at, and one came in with the new tests. A cancellation token that was not the last parameter; a log argument sanitized on the way into a call that may discard it, on a path that runs for every event of a live call; an Assert.Single filtered by a Where clause instead of the overload that takes the predicate; a test helper whose dialect parameter was an interface every caller resolved to the same concrete type, now stated explicitly by each caller because some of those statements genuinely differ between SQLite and PostgreSQL; and six awaits in tests that did not carry the test's own token. Verified the whole way through: the solution builds clean under the pipeline's own switches, and both suites the pipeline runs pass -- 5,294 and 76. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No description provided.