@sayknow-cli/coding-agent 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +30 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +8 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +12 -3
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon.d.ts +45 -3
- package/dist/types/notifications/telegram-reference.d.ts +7 -5
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +61 -6
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/skc-runtime/state-writer.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +8 -8
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +61 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +103 -17
- package/src/cli/telegram-cli.ts +1 -1
- package/src/cli/update-cli.ts +148 -33
- package/src/cli.ts +29 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-profiles.ts +31 -2
- package/src/config/settings-schema.ts +76 -1
- package/src/config/settings.ts +22 -0
- package/src/defaults/skc/skills/team/SKILL.md +17 -14
- package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/skc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +183 -219
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +124 -4
- package/src/internal-urls/docs-index.generated.ts +17 -8
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +95 -43
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +181 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +12 -11
- package/src/modes/controllers/input-controller.ts +192 -35
- package/src/modes/controllers/selector-controller.ts +168 -24
- package/src/modes/interactive-mode.ts +56 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +20 -7
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +52 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +144 -18
- package/src/notifications/lifecycle-commands.ts +74 -23
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +606 -98
- package/src/notifications/telegram-reference.ts +17 -16
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +29 -17
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/computer.md +2 -2
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +88 -36
- package/src/session/agent-session.ts +1013 -141
- package/src/session/agent-storage.ts +4 -0
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +103 -4
- package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/skc-runtime/deep-interview-recorder.ts +62 -8
- package/src/skc-runtime/deep-interview-state.ts +44 -2
- package/src/skc-runtime/session-state-sidecar.ts +263 -20
- package/src/skc-runtime/state-runtime.ts +12 -2
- package/src/skc-runtime/state-writer.ts +15 -0
- package/src/skc-runtime/tmux-common.ts +1 -0
- package/src/skc-runtime/ultragoal-guard.ts +55 -66
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/skc-runtime/ultragoal-runtime.ts +1672 -159
- package/src/skc-runtime/workflow-manifest.generated.json +113 -2
- package/src/skc-runtime/workflow-manifest.ts +74 -1
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +296 -72
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +132 -24
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +13 -6
- package/src/task/output-manager.ts +20 -8
- package/src/task/receipt.ts +26 -7
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/computer.ts +246 -43
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/tools/telegram-send.ts +6 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -420,6 +420,10 @@ FROM model_usage_legacy
|
|
|
420
420
|
this.#authStore.setCache(key, value, expiresAtSec);
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
deleteCachePrefix(prefix: string): void {
|
|
424
|
+
this.#authStore.deleteCachePrefix?.(prefix);
|
|
425
|
+
}
|
|
426
|
+
|
|
423
427
|
/**
|
|
424
428
|
* Deletes expired cache entries. Call periodically for cleanup.
|
|
425
429
|
*/
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staleness pruning for auto-read file-mention entries.
|
|
3
|
+
*
|
|
4
|
+
* A `@path` mention is superseded once a newer read or mention of the same
|
|
5
|
+
* resolved path appears later in the branch. Stale bodies are replaced with an
|
|
6
|
+
* explicit digest notice (never silently deleted) so context shrinks without
|
|
7
|
+
* losing the reference.
|
|
8
|
+
*/
|
|
9
|
+
import type { FileMentionMessage } from "./messages";
|
|
10
|
+
import type { SessionEntry, SessionMessageEntry } from "./session-manager";
|
|
11
|
+
|
|
12
|
+
export interface FileMentionPruneResult {
|
|
13
|
+
changed: SessionMessageEntry[];
|
|
14
|
+
bytesSaved: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Notice injected in place of a superseded file-mention body. */
|
|
18
|
+
export function stalefileMentionNotice(path: string): string {
|
|
19
|
+
return `(pruned: a newer view of \`${path}\` appears later in the conversation; use the read tool to fetch it again)`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function pruneStaleFileMentions(
|
|
23
|
+
entries: readonly SessionEntry[],
|
|
24
|
+
resolveAbs: (path: string) => string,
|
|
25
|
+
): FileMentionPruneResult {
|
|
26
|
+
// Latest branch index at which each absolute path was shown (read or mention).
|
|
27
|
+
const latestShownIndex = new Map<string, number>();
|
|
28
|
+
entries.forEach((entry, i) => {
|
|
29
|
+
if (entry.type !== "message") return;
|
|
30
|
+
const msg = entry.message;
|
|
31
|
+
if (msg.role === "fileMention") {
|
|
32
|
+
for (const file of msg.files) {
|
|
33
|
+
if (file.duplicate || file.pruned) continue;
|
|
34
|
+
latestShownIndex.set(resolveAbs(file.path), i);
|
|
35
|
+
}
|
|
36
|
+
} else if (msg.role === "toolResult") {
|
|
37
|
+
const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
|
|
38
|
+
if (typeof resolved === "string" && resolved) latestShownIndex.set(resolveAbs(resolved), i);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const changed: SessionMessageEntry[] = [];
|
|
43
|
+
let bytesSaved = 0;
|
|
44
|
+
entries.forEach((entry, i) => {
|
|
45
|
+
if (entry.type !== "message" || entry.message.role !== "fileMention") return;
|
|
46
|
+
const msg = entry.message as FileMentionMessage;
|
|
47
|
+
let mutated = false;
|
|
48
|
+
for (const file of msg.files) {
|
|
49
|
+
if (file.duplicate || file.pruned || !file.content) continue;
|
|
50
|
+
const latest = latestShownIndex.get(resolveAbs(file.path));
|
|
51
|
+
if (latest !== undefined && latest > i) {
|
|
52
|
+
const notice = stalefileMentionNotice(file.path);
|
|
53
|
+
bytesSaved += Math.max(0, Buffer.byteLength(file.content, "utf-8") - Buffer.byteLength(notice, "utf-8"));
|
|
54
|
+
file.content = notice;
|
|
55
|
+
file.pruned = true;
|
|
56
|
+
file.image = undefined;
|
|
57
|
+
file.lineCount = undefined;
|
|
58
|
+
mutated = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (mutated) changed.push(entry as SessionMessageEntry);
|
|
62
|
+
});
|
|
63
|
+
return { changed, bytesSaved };
|
|
64
|
+
}
|
package/src/session/messages.ts
CHANGED
|
@@ -192,6 +192,10 @@ export interface FileMentionMessage {
|
|
|
192
192
|
/** Why the file contents were omitted from auto-read. */
|
|
193
193
|
skippedReason?: "tooLarge";
|
|
194
194
|
image?: ImageContent;
|
|
195
|
+
/** Set when this mention duplicated a recently shown path (compact note, no full body). */
|
|
196
|
+
duplicate?: boolean;
|
|
197
|
+
/** Set when a stale entry was pruned to a digest notice (never silently deleted). */
|
|
198
|
+
pruned?: boolean;
|
|
195
199
|
}>;
|
|
196
200
|
timestamp: number;
|
|
197
201
|
}
|
|
@@ -27,7 +27,9 @@ import {
|
|
|
27
27
|
Snowflake,
|
|
28
28
|
toError,
|
|
29
29
|
} from "@sayknow-cli/utils";
|
|
30
|
+
import type { TtsrInjectionRecord } from "../export/ttsr";
|
|
30
31
|
import { writeTextAtomic } from "../skc-runtime/state-writer";
|
|
32
|
+
|
|
31
33
|
import * as git from "../utils/git";
|
|
32
34
|
import { ArtifactManager } from "./artifacts";
|
|
33
35
|
import {
|
|
@@ -221,6 +223,10 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
|
|
|
221
223
|
type: "ttsr_injection";
|
|
222
224
|
/** Names of rules that were injected */
|
|
223
225
|
injectedRules: string[];
|
|
226
|
+
/** Rich rule injection records with repeat state. */
|
|
227
|
+
injectedRuleRecords?: TtsrInjectionRecord[];
|
|
228
|
+
/** TTSR manager message count when this injection was recorded. */
|
|
229
|
+
ttsrMessageCount?: number;
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
/** Persisted MCP discovery selection state for a session branch. */
|
|
@@ -313,6 +319,11 @@ export interface SessionContext {
|
|
|
313
319
|
models: Record<string, string>;
|
|
314
320
|
/** Names of TTSR rules that have been injected this session */
|
|
315
321
|
injectedTtsrRules: string[];
|
|
322
|
+
/** Rich TTSR rule injection records for repeat resume. */
|
|
323
|
+
injectedTtsrRuleRecords?: TtsrInjectionRecord[];
|
|
324
|
+
/** TTSR manager message count for repeat resume. */
|
|
325
|
+
ttsrMessageCount?: number;
|
|
326
|
+
|
|
316
327
|
/** MCP tool names selected through discovery for this session branch. */
|
|
317
328
|
selectedMCPToolNames: string[];
|
|
318
329
|
/** Whether this branch contains an explicit persisted MCP selection entry. */
|
|
@@ -612,6 +623,9 @@ export function buildSessionContext(
|
|
|
612
623
|
serviceTier: undefined,
|
|
613
624
|
models: {},
|
|
614
625
|
injectedTtsrRules: [],
|
|
626
|
+
injectedTtsrRuleRecords: [],
|
|
627
|
+
ttsrMessageCount: 0,
|
|
628
|
+
|
|
615
629
|
selectedMCPToolNames: [],
|
|
616
630
|
hasPersistedMCPToolSelection: false,
|
|
617
631
|
mode: "none",
|
|
@@ -632,6 +646,8 @@ export function buildSessionContext(
|
|
|
632
646
|
serviceTier: undefined,
|
|
633
647
|
models: {},
|
|
634
648
|
injectedTtsrRules: [],
|
|
649
|
+
injectedTtsrRuleRecords: [],
|
|
650
|
+
ttsrMessageCount: 0,
|
|
635
651
|
selectedMCPToolNames: [],
|
|
636
652
|
hasPersistedMCPToolSelection: false,
|
|
637
653
|
mode: "none",
|
|
@@ -656,6 +672,9 @@ export function buildSessionContext(
|
|
|
656
672
|
const models: Record<string, string> = {};
|
|
657
673
|
let compaction: CompactionEntry | null = null;
|
|
658
674
|
const injectedTtsrRulesSet = new Set<string>();
|
|
675
|
+
const injectedTtsrRuleRecords = new Map<string, TtsrInjectionRecord>();
|
|
676
|
+
let ttsrMessageCount = 0;
|
|
677
|
+
|
|
659
678
|
let selectedMCPToolNames: string[] = [];
|
|
660
679
|
let hasPersistedMCPToolSelection = false;
|
|
661
680
|
let mode = "none";
|
|
@@ -695,9 +714,19 @@ export function buildSessionContext(
|
|
|
695
714
|
} else if (entry.type === "compaction") {
|
|
696
715
|
compaction = entry;
|
|
697
716
|
} else if (entry.type === "ttsr_injection") {
|
|
698
|
-
// Collect injected TTSR rule names
|
|
717
|
+
// Collect injected TTSR rule names and richer records when present.
|
|
699
718
|
for (const ruleName of entry.injectedRules) {
|
|
700
719
|
injectedTtsrRulesSet.add(ruleName);
|
|
720
|
+
if (!injectedTtsrRuleRecords.has(ruleName)) {
|
|
721
|
+
injectedTtsrRuleRecords.set(ruleName, { name: ruleName, lastInjectedAt: 0 });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
for (const record of entry.injectedRuleRecords ?? []) {
|
|
725
|
+
injectedTtsrRulesSet.add(record.name);
|
|
726
|
+
injectedTtsrRuleRecords.set(record.name, record);
|
|
727
|
+
}
|
|
728
|
+
if (typeof entry.ttsrMessageCount === "number" && Number.isFinite(entry.ttsrMessageCount)) {
|
|
729
|
+
ttsrMessageCount = entry.ttsrMessageCount;
|
|
701
730
|
}
|
|
702
731
|
} else if (entry.type === "mcp_tool_selection") {
|
|
703
732
|
selectedMCPToolNames = [...entry.selectedToolNames];
|
|
@@ -709,6 +738,7 @@ export function buildSessionContext(
|
|
|
709
738
|
}
|
|
710
739
|
|
|
711
740
|
const injectedTtsrRules = Array.from(injectedTtsrRulesSet);
|
|
741
|
+
const injectedTtsrRuleRecordsArray = Array.from(injectedTtsrRuleRecords.values());
|
|
712
742
|
|
|
713
743
|
// Build messages and collect corresponding entries
|
|
714
744
|
// When there's a compaction, we need to:
|
|
@@ -797,6 +827,9 @@ export function buildSessionContext(
|
|
|
797
827
|
serviceTier,
|
|
798
828
|
models,
|
|
799
829
|
injectedTtsrRules,
|
|
830
|
+
injectedTtsrRuleRecords: injectedTtsrRuleRecordsArray,
|
|
831
|
+
ttsrMessageCount,
|
|
832
|
+
|
|
800
833
|
selectedMCPToolNames,
|
|
801
834
|
hasPersistedMCPToolSelection,
|
|
802
835
|
mode,
|
|
@@ -810,6 +843,9 @@ function cloneSessionContext(context: SessionContext): SessionContext {
|
|
|
810
843
|
messages: cloneJsonSemantic(context.messages),
|
|
811
844
|
models: { ...context.models },
|
|
812
845
|
injectedTtsrRules: [...context.injectedTtsrRules],
|
|
846
|
+
injectedTtsrRuleRecords: context.injectedTtsrRuleRecords?.map(record => ({ ...record })),
|
|
847
|
+
ttsrMessageCount: context.ttsrMessageCount,
|
|
848
|
+
|
|
813
849
|
selectedMCPToolNames: [...context.selectedMCPToolNames],
|
|
814
850
|
modeData: cloneJsonSemantic(context.modeData),
|
|
815
851
|
};
|
|
@@ -1308,6 +1344,45 @@ function isImageBlock(value: unknown): value is { type: "image"; data: string; m
|
|
|
1308
1344
|
);
|
|
1309
1345
|
}
|
|
1310
1346
|
|
|
1347
|
+
function stripUndefinedPlainObjectFields(value: unknown, path = "entry"): unknown {
|
|
1348
|
+
if (value === undefined) return undefined;
|
|
1349
|
+
if (value === null || typeof value !== "object") return value;
|
|
1350
|
+
if (Array.isArray(value)) {
|
|
1351
|
+
let changed = false;
|
|
1352
|
+
const result: unknown[] = new Array(value.length);
|
|
1353
|
+
for (let index = 0; index < value.length; index++) {
|
|
1354
|
+
const item = value[index];
|
|
1355
|
+
if (item === undefined) {
|
|
1356
|
+
throw new Error(`Session entry contains undefined array item at ${path}[${index}]`);
|
|
1357
|
+
}
|
|
1358
|
+
const next = stripUndefinedPlainObjectFields(item, `${path}[${index}]`);
|
|
1359
|
+
if (next !== item) changed = true;
|
|
1360
|
+
result[index] = next;
|
|
1361
|
+
}
|
|
1362
|
+
return changed ? result : value;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1366
|
+
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
1367
|
+
|
|
1368
|
+
let changed = false;
|
|
1369
|
+
const entries: Array<readonly [string, unknown]> = [];
|
|
1370
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1371
|
+
if (item === undefined) {
|
|
1372
|
+
changed = true;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
const next = stripUndefinedPlainObjectFields(item, `${path}.${key}`);
|
|
1376
|
+
if (next !== item) changed = true;
|
|
1377
|
+
entries.push([key, next]);
|
|
1378
|
+
}
|
|
1379
|
+
return changed ? Object.fromEntries(entries) : value;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function normalizeSessionEntryForStorage(entry: SessionEntry): SessionEntry {
|
|
1383
|
+
return stripUndefinedPlainObjectFields(entry) as SessionEntry;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1311
1386
|
const RESIDENT_EXTERNALIZE_STRING_EXCLUDED_KEYS = new Set([
|
|
1312
1387
|
"id",
|
|
1313
1388
|
"type",
|
|
@@ -2254,10 +2329,12 @@ class NdjsonFileWriter {
|
|
|
2254
2329
|
}
|
|
2255
2330
|
}
|
|
2256
2331
|
|
|
2332
|
+
const DEFAULT_WELCOME_RECENT_SESSION_LIMIT = 20;
|
|
2333
|
+
|
|
2257
2334
|
/** Get recent sessions for display in welcome screen */
|
|
2258
2335
|
export async function getRecentSessions(
|
|
2259
2336
|
sessionDir: string,
|
|
2260
|
-
limit =
|
|
2337
|
+
limit = DEFAULT_WELCOME_RECENT_SESSION_LIMIT,
|
|
2261
2338
|
storage: SessionStorage = new FileSessionStorage(),
|
|
2262
2339
|
): Promise<RecentSessionInfo[]> {
|
|
2263
2340
|
const sessions = await getSortedSessions(sessionDir, storage);
|
|
@@ -3734,7 +3811,8 @@ export class SessionManager {
|
|
|
3734
3811
|
}
|
|
3735
3812
|
|
|
3736
3813
|
#appendEntry(entry: SessionEntry): void {
|
|
3737
|
-
const
|
|
3814
|
+
const normalizedEntry = normalizeSessionEntryForStorage(entry);
|
|
3815
|
+
const residentEntry = prepareEntryForResidentSync(normalizedEntry, this.#residentBlobStores()) as SessionEntry;
|
|
3738
3816
|
this.#fileEntries.push(residentEntry);
|
|
3739
3817
|
this.#byId.set(residentEntry.id, residentEntry);
|
|
3740
3818
|
this.#leafId = residentEntry.id;
|
|
@@ -3915,6 +3993,25 @@ export class SessionManager {
|
|
|
3915
3993
|
return entry.id;
|
|
3916
3994
|
}
|
|
3917
3995
|
|
|
3996
|
+
/**
|
|
3997
|
+
* Append a root marker that starts a fresh active branch without changing the
|
|
3998
|
+
* session id or deleting earlier durable entries. Subsequent messages descend
|
|
3999
|
+
* from this marker, so provider context is clear while history remains
|
|
4000
|
+
* available for diagnostics/export.
|
|
4001
|
+
*/
|
|
4002
|
+
appendContextClearEntry(data?: Record<string, unknown>): string {
|
|
4003
|
+
const entry: CustomEntry = {
|
|
4004
|
+
type: "custom",
|
|
4005
|
+
customType: "context_clear",
|
|
4006
|
+
data,
|
|
4007
|
+
id: generateId(this.#byId),
|
|
4008
|
+
parentId: null,
|
|
4009
|
+
timestamp: new Date().toISOString(),
|
|
4010
|
+
};
|
|
4011
|
+
this.#appendEntry(entry);
|
|
4012
|
+
return entry.id;
|
|
4013
|
+
}
|
|
4014
|
+
|
|
3918
4015
|
/**
|
|
3919
4016
|
* Write mutated message entries back into the canonical entry store by id.
|
|
3920
4017
|
*
|
|
@@ -4006,13 +4103,15 @@ export class SessionManager {
|
|
|
4006
4103
|
* @param ruleNames Names of rules that were injected
|
|
4007
4104
|
* @returns Entry id
|
|
4008
4105
|
*/
|
|
4009
|
-
appendTtsrInjection(ruleNames: string[]): string {
|
|
4106
|
+
appendTtsrInjection(ruleNames: string[], records?: TtsrInjectionRecord[], ttsrMessageCount?: number): string {
|
|
4010
4107
|
const entry: TtsrInjectionEntry = {
|
|
4011
4108
|
type: "ttsr_injection",
|
|
4012
4109
|
id: generateId(this.#byId),
|
|
4013
4110
|
parentId: this.#leafId,
|
|
4014
4111
|
timestamp: new Date().toISOString(),
|
|
4015
4112
|
injectedRules: ruleNames,
|
|
4113
|
+
injectedRuleRecords: records,
|
|
4114
|
+
ttsrMessageCount,
|
|
4016
4115
|
};
|
|
4017
4116
|
this.#appendEntry(entry);
|
|
4018
4117
|
return entry.id;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DeepInterviewEstablishedFact,
|
|
3
|
+
type DeepInterviewRoundRecord,
|
|
4
|
+
type DeepInterviewStateEnvelope,
|
|
5
|
+
normalizeDeepInterviewEnvelope,
|
|
6
|
+
} from "./deep-interview-state";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deterministic ambiguity floor for deep-interview.
|
|
10
|
+
*
|
|
11
|
+
* The interview's ambiguity score is reported by an LLM scorer, which anchors on
|
|
12
|
+
* prior scores and under-reports rises when the user pivots or contradicts earlier
|
|
13
|
+
* answers. Following the Ouroboros `max(llm_score, deterministic_floor(ledger))`
|
|
14
|
+
* principle, this pure leaf module computes a code-level lower bound from evidence
|
|
15
|
+
* already persisted in deep-interview state, so the reported score can never fall
|
|
16
|
+
* below what code can objectively measure:
|
|
17
|
+
*
|
|
18
|
+
* - `0.10` per established fact marked disputed with no `superseded_by` resolution
|
|
19
|
+
* (contradiction pressure — a pivot keeps ambiguity elevated until resolved);
|
|
20
|
+
* - `0.05` per active topology component whose goal/constraints/criteria clarity
|
|
21
|
+
* is still unscored (gap pressure — a sibling component cannot hide);
|
|
22
|
+
* - `0.05 × (auto-answered rounds / scored rounds)` (assumption dilution).
|
|
23
|
+
*
|
|
24
|
+
* Like `deep-interview-state`, this module MUST stay pure and dependency-free
|
|
25
|
+
* (no filesystem, no state-writer, no CLI runtime) so every writer can apply it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Weights (mirrors the Ouroboros deterministic_floor coefficients)
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
const DISPUTED_FACT_WEIGHT = 0.1;
|
|
33
|
+
const UNSCORED_COMPONENT_WEIGHT = 0.05;
|
|
34
|
+
const AUTO_ANSWER_DILUTION_WEIGHT = 0.05;
|
|
35
|
+
|
|
36
|
+
const CORE_CLARITY_DIMENSIONS = ["goal", "constraints", "criteria"] as const;
|
|
37
|
+
|
|
38
|
+
export interface AmbiguityFloorBreakdown {
|
|
39
|
+
floor: number;
|
|
40
|
+
disputed_fact_count: number;
|
|
41
|
+
unscored_active_component_count: number;
|
|
42
|
+
auto_answer_ratio: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AmbiguityClampResult {
|
|
46
|
+
effective: number;
|
|
47
|
+
clamped: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Pure helpers
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
55
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function asArray(value: unknown): unknown[] {
|
|
59
|
+
return Array.isArray(value) ? value : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function round2(value: number): number {
|
|
63
|
+
return Math.round(value * 100) / 100;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A disputed fact contributes pressure until it is resolved via `superseded_by` or re-confirmation. */
|
|
67
|
+
function isUnresolvedDisputedFact(value: unknown): boolean {
|
|
68
|
+
if (!isPlainObject(value)) return false;
|
|
69
|
+
const fact = value as Partial<DeepInterviewEstablishedFact>;
|
|
70
|
+
if (fact.disputed !== true) return false;
|
|
71
|
+
return typeof fact.superseded_by !== "string" || fact.superseded_by.trim() === "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* An active component is unscored while any core clarity dimension lacks a finite
|
|
76
|
+
* numeric score. Deferred components are excluded (they are outside ambiguity math),
|
|
77
|
+
* and the gate only arms once the topology was explicitly confirmed in Round 0.
|
|
78
|
+
*/
|
|
79
|
+
function countUnscoredActiveComponents(topology: unknown): number {
|
|
80
|
+
if (!isPlainObject(topology) || topology.status !== "confirmed") return 0;
|
|
81
|
+
let unscored = 0;
|
|
82
|
+
for (const component of asArray(topology.components)) {
|
|
83
|
+
if (!isPlainObject(component) || component.status === "deferred") continue;
|
|
84
|
+
const clarity = isPlainObject(component.clarity_scores) ? component.clarity_scores : {};
|
|
85
|
+
const incomplete = CORE_CLARITY_DIMENSIONS.some(dimension => {
|
|
86
|
+
const score = clarity[dimension];
|
|
87
|
+
return typeof score !== "number" || !Number.isFinite(score);
|
|
88
|
+
});
|
|
89
|
+
if (incomplete) unscored += 1;
|
|
90
|
+
}
|
|
91
|
+
return unscored;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function autoAnswerRatio(inner: Record<string, unknown>): number {
|
|
95
|
+
const autoAnswered = asArray(inner.auto_answered_rounds).length;
|
|
96
|
+
if (autoAnswered === 0) return 0;
|
|
97
|
+
const scored = asArray(inner.rounds).filter(round => isPlainObject(round) && round.lifecycle === "scored").length;
|
|
98
|
+
return Math.min(1, autoAnswered / Math.max(scored, 1));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Floor computation + clamping
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
/** Compute the deterministic floor from the inner deep-interview `state` object. */
|
|
106
|
+
export function computeAmbiguityFloor(inner: unknown): AmbiguityFloorBreakdown {
|
|
107
|
+
const state = isPlainObject(inner) ? inner : {};
|
|
108
|
+
const disputedFactCount = asArray(state.established_facts).filter(isUnresolvedDisputedFact).length;
|
|
109
|
+
const unscoredActiveComponentCount = countUnscoredActiveComponents(state.topology);
|
|
110
|
+
const ratio = autoAnswerRatio(state);
|
|
111
|
+
const floor =
|
|
112
|
+
DISPUTED_FACT_WEIGHT * disputedFactCount +
|
|
113
|
+
UNSCORED_COMPONENT_WEIGHT * unscoredActiveComponentCount +
|
|
114
|
+
AUTO_ANSWER_DILUTION_WEIGHT * ratio;
|
|
115
|
+
return {
|
|
116
|
+
floor: round2(Math.min(1, Math.max(0, floor))),
|
|
117
|
+
disputed_fact_count: disputedFactCount,
|
|
118
|
+
unscored_active_component_count: unscoredActiveComponentCount,
|
|
119
|
+
auto_answer_ratio: round2(ratio),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Clamp an LLM-reported ambiguity to the deterministic floor: `max(reported, floor)`. */
|
|
124
|
+
export function clampReportedAmbiguity(reported: number, floor: number): AmbiguityClampResult {
|
|
125
|
+
const bounded = Math.min(1, Math.max(0, reported));
|
|
126
|
+
if (floor > bounded) return { effective: Math.min(1, floor), clamped: true };
|
|
127
|
+
return { effective: bounded, clamped: false };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Mark every established fact recorded in `retractedRound` as disputed. This is the
|
|
132
|
+
* mechanical contradiction signal for answer retraction: when the user replaces an
|
|
133
|
+
* already-scored answer (A -> B pivot), the facts that answer established can no
|
|
134
|
+
* longer be trusted, so they raise the floor until re-confirmed or superseded.
|
|
135
|
+
* Returns new arrays/objects; never mutates the input.
|
|
136
|
+
*/
|
|
137
|
+
export function disputeFactsFromRetractedRound(
|
|
138
|
+
facts: readonly unknown[],
|
|
139
|
+
retractedRound: number,
|
|
140
|
+
): { facts: Record<string, unknown>[]; disputedIds: string[] } {
|
|
141
|
+
const disputedIds: string[] = [];
|
|
142
|
+
const next = facts.filter(isPlainObject).map(fact => {
|
|
143
|
+
const record = fact as Record<string, unknown>;
|
|
144
|
+
if (record.round !== retractedRound || record.disputed === true) return { ...record };
|
|
145
|
+
if (typeof record.superseded_by === "string" && record.superseded_by.trim() !== "") return { ...record };
|
|
146
|
+
if (typeof record.id === "string") disputedIds.push(record.id);
|
|
147
|
+
return { ...record, disputed: true };
|
|
148
|
+
});
|
|
149
|
+
return { facts: next, disputedIds };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface AppliedAmbiguityFloor {
|
|
153
|
+
envelope: DeepInterviewStateEnvelope;
|
|
154
|
+
breakdown: AmbiguityFloorBreakdown;
|
|
155
|
+
clamped: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Enforce the floor invariant on a full deep-interview envelope: recompute the floor
|
|
160
|
+
* from persisted evidence, clamp `state.current_ambiguity`, and clamp the latest
|
|
161
|
+
* scored round (preserving the original value as `reported_ambiguity` for audit).
|
|
162
|
+
* Historical rounds are never rewritten. Idempotent and non-mutating; every writer
|
|
163
|
+
* (state CLI write/reconcile, recorder) applies this immediately before persisting.
|
|
164
|
+
*/
|
|
165
|
+
export function applyAmbiguityFloorToEnvelope(value: unknown): AppliedAmbiguityFloor {
|
|
166
|
+
const envelope = normalizeDeepInterviewEnvelope(value);
|
|
167
|
+
const inner = { ...(envelope.state as Record<string, unknown>) };
|
|
168
|
+
const breakdown = computeAmbiguityFloor(inner);
|
|
169
|
+
let clamped = false;
|
|
170
|
+
|
|
171
|
+
const rounds = asArray(inner.rounds).filter(isPlainObject) as unknown as DeepInterviewRoundRecord[];
|
|
172
|
+
let latestScoredIndex = -1;
|
|
173
|
+
for (let index = 0; index < rounds.length; index += 1) {
|
|
174
|
+
const candidate = rounds[index];
|
|
175
|
+
if (candidate.lifecycle !== "scored" || !Number.isFinite(candidate.round)) continue;
|
|
176
|
+
if (latestScoredIndex < 0 || candidate.round >= rounds[latestScoredIndex].round) latestScoredIndex = index;
|
|
177
|
+
}
|
|
178
|
+
if (latestScoredIndex >= 0) {
|
|
179
|
+
const latest = rounds[latestScoredIndex];
|
|
180
|
+
if (typeof latest.ambiguity === "number") {
|
|
181
|
+
const clampedRound = clampReportedAmbiguity(latest.ambiguity, breakdown.floor);
|
|
182
|
+
if (clampedRound.clamped) {
|
|
183
|
+
const nextRounds = [...rounds];
|
|
184
|
+
nextRounds[latestScoredIndex] = {
|
|
185
|
+
...latest,
|
|
186
|
+
reported_ambiguity: latest.reported_ambiguity ?? latest.ambiguity,
|
|
187
|
+
ambiguity: clampedRound.effective,
|
|
188
|
+
ambiguity_floor: breakdown.floor,
|
|
189
|
+
};
|
|
190
|
+
inner.rounds = nextRounds;
|
|
191
|
+
clamped = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof inner.current_ambiguity === "number") {
|
|
197
|
+
const clampedCurrent = clampReportedAmbiguity(inner.current_ambiguity, breakdown.floor);
|
|
198
|
+
if (clampedCurrent.clamped) {
|
|
199
|
+
inner.current_ambiguity = clampedCurrent.effective;
|
|
200
|
+
clamped = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
inner.ambiguity_floor = breakdown;
|
|
205
|
+
return { envelope: { ...envelope, state: inner }, breakdown, clamped };
|
|
206
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
2
2
|
import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
|
|
3
3
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
4
|
+
import {
|
|
5
|
+
clampReportedAmbiguity,
|
|
6
|
+
computeAmbiguityFloor,
|
|
7
|
+
disputeFactsFromRetractedRound,
|
|
8
|
+
} from "./deep-interview-ambiguity";
|
|
4
9
|
import {
|
|
5
10
|
answerHash,
|
|
6
11
|
type DeepInterviewEstablishedFact,
|
|
@@ -14,6 +19,7 @@ import {
|
|
|
14
19
|
import { writeSessionActivityMarker } from "./session-resolution";
|
|
15
20
|
import { readExistingStateForMutation, writeGuardedWorkflowEnvelopeAtomic } from "./state-writer";
|
|
16
21
|
|
|
22
|
+
export * from "./deep-interview-ambiguity";
|
|
17
23
|
export * from "./deep-interview-state";
|
|
18
24
|
|
|
19
25
|
/**
|
|
@@ -395,30 +401,55 @@ export async function syncDeepInterviewRecorderHud(
|
|
|
395
401
|
await syncRecorderHud(cwd, normalizeDeepInterviewEnvelope(read.value), sessionId);
|
|
396
402
|
}
|
|
397
403
|
|
|
398
|
-
/**
|
|
404
|
+
/**
|
|
405
|
+
* Record an `answered` shell for one round (append-or-merge by durable key).
|
|
406
|
+
*
|
|
407
|
+
* Retraction dynamics: replacing an already-`scored` answer is a mechanical
|
|
408
|
+
* contradiction signal (the user pivoted). The facts that round established are
|
|
409
|
+
* marked disputed, which raises the deterministic ambiguity floor immediately —
|
|
410
|
+
* without waiting for the LLM scorer to self-report a trigger.
|
|
411
|
+
*/
|
|
399
412
|
export async function appendOrMergeDeepInterviewRound(
|
|
400
413
|
cwd: string,
|
|
401
414
|
statePath: string,
|
|
402
415
|
input: DeepInterviewAnswerInput,
|
|
403
416
|
options: { sessionId?: string } = {},
|
|
404
|
-
): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord }> {
|
|
417
|
+
): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord; disputedFactIds?: string[] }> {
|
|
405
418
|
const envelope = await readEnvelope(statePath);
|
|
406
419
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
407
420
|
const shell = buildAnswerShell({ ...input, interviewId });
|
|
408
421
|
const rounds = readRounds(envelope);
|
|
422
|
+
const priorRecord = rounds.find(r => r.round_key === shell.round_key);
|
|
409
423
|
const result = appendOrMergeRound(rounds, shell);
|
|
410
424
|
if (result.action === "noop") {
|
|
411
425
|
await repairRecorderHudFromPersisted(cwd, statePath, options.sessionId);
|
|
412
426
|
return { action: result.action, record: result.record };
|
|
413
427
|
}
|
|
414
|
-
|
|
428
|
+
const inner = envelope.state as Record<string, unknown>;
|
|
429
|
+
inner.rounds = result.rounds;
|
|
430
|
+
let disputedFactIds: string[] | undefined;
|
|
431
|
+
if (result.action === "replaced" && priorRecord?.lifecycle === "scored") {
|
|
432
|
+
const facts = Array.isArray(inner.established_facts) ? inner.established_facts : [];
|
|
433
|
+
const disputed = disputeFactsFromRetractedRound(facts, shell.round);
|
|
434
|
+
if (disputed.disputedIds.length > 0) {
|
|
435
|
+
inner.established_facts = disputed.facts;
|
|
436
|
+
disputedFactIds = disputed.disputedIds;
|
|
437
|
+
// Surface the rise right away: the retraction changed the evidence, so the
|
|
438
|
+
// gating score must reflect the new floor before the next scoring pass.
|
|
439
|
+
const breakdown = computeAmbiguityFloor(inner);
|
|
440
|
+
inner.ambiguity_floor = breakdown;
|
|
441
|
+
if (typeof inner.current_ambiguity === "number") {
|
|
442
|
+
inner.current_ambiguity = clampReportedAmbiguity(inner.current_ambiguity, breakdown.floor).effective;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
415
446
|
await persistEnvelope(cwd, statePath, envelope, options.sessionId, "skc deep-interview record-answer");
|
|
416
447
|
try {
|
|
417
448
|
await syncRecorderHud(cwd, envelope, options.sessionId);
|
|
418
449
|
} catch {
|
|
419
450
|
// HUD sync is best-effort cache maintenance and must not change record semantics.
|
|
420
451
|
}
|
|
421
|
-
return { action: result.action, record: result.record };
|
|
452
|
+
return { action: result.action, record: result.record, disputedFactIds };
|
|
422
453
|
}
|
|
423
454
|
|
|
424
455
|
/**
|
|
@@ -459,12 +490,34 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
459
490
|
const envelope = await readEnvelope(statePath);
|
|
460
491
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
461
492
|
const rounds = readRounds(envelope);
|
|
462
|
-
const { rounds:
|
|
493
|
+
const { rounds: enrichedRounds, record: reportedRecord } = enrichRoundWithScoring(rounds, {
|
|
494
|
+
...input,
|
|
495
|
+
interviewId,
|
|
496
|
+
});
|
|
497
|
+
// Deterministic floor (Ouroboros principle): the LLM-reported ambiguity is only a
|
|
498
|
+
// lower-bound input. Evidence persisted in state — unresolved disputed facts,
|
|
499
|
+
// unscored active components, auto-answer dilution — sets a code-computed floor
|
|
500
|
+
// the reported score can never fall under, so a pivot mechanically raises the
|
|
501
|
+
// gating score even when the scorer omits triggers.
|
|
502
|
+
const inner = envelope.state as Record<string, unknown>;
|
|
503
|
+
const breakdown = computeAmbiguityFloor({ ...inner, rounds: enrichedRounds });
|
|
504
|
+
const clampResult = clampReportedAmbiguity(input.ambiguity, breakdown.floor);
|
|
505
|
+
let record = reportedRecord;
|
|
506
|
+
let nextRounds = enrichedRounds;
|
|
507
|
+
if (clampResult.clamped) {
|
|
508
|
+
record = {
|
|
509
|
+
...reportedRecord,
|
|
510
|
+
ambiguity: clampResult.effective,
|
|
511
|
+
reported_ambiguity: input.ambiguity,
|
|
512
|
+
ambiguity_floor: breakdown.floor,
|
|
513
|
+
};
|
|
514
|
+
nextRounds = enrichedRounds.map(item => (item.round_key === record.round_key ? record : item));
|
|
515
|
+
}
|
|
463
516
|
// Fail closed: a scored transition that violates the bidirectional invariant
|
|
464
517
|
// (an active trigger that improves the affected dimension or fails to raise
|
|
465
518
|
// overall ambiguity, or a disputed/unresolved trigger lacking a rationale) must
|
|
466
519
|
// never be persisted — storing it lets the interview falsely converge. Validate
|
|
467
|
-
// against the most recent prior scored round
|
|
520
|
+
// the effective (floor-clamped) record against the most recent prior scored round.
|
|
468
521
|
const prior = latestPriorScoredRound(rounds, record.round_key, record.round);
|
|
469
522
|
const validation = validateDeepInterviewScoredTransition(prior, record);
|
|
470
523
|
if (!validation.ok) {
|
|
@@ -472,8 +525,9 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
472
525
|
`deep-interview scored transition for round ${record.round} is invalid and was refused: ${validation.violations.join("; ")}`,
|
|
473
526
|
);
|
|
474
527
|
}
|
|
475
|
-
|
|
476
|
-
|
|
528
|
+
inner.rounds = nextRounds;
|
|
529
|
+
inner.current_ambiguity = clampResult.effective;
|
|
530
|
+
inner.ambiguity_floor = breakdown;
|
|
477
531
|
await persistEnvelope(cwd, statePath, envelope, options.sessionId, "skc deep-interview score-round");
|
|
478
532
|
await syncRecorderHud(cwd, envelope, options.sessionId);
|
|
479
533
|
return { record };
|