@scotthuang/agent-knock-knock 0.13.2 → 0.13.3
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/CHANGELOG.md +22 -0
- package/README.md +17 -4
- package/dist/src/claude-terminal-agent-adapter.js +4 -0
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli-core.js +9 -1
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js +4 -0
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/herdr-terminal-control-provider.js +6 -1
- package/dist/src/herdr-terminal-control-provider.js.map +1 -1
- package/dist/src/host-adapter.d.ts +1 -1
- package/dist/src/host-adapter.js +1 -1
- package/dist/src/host-bridge-tools.js +2 -2
- package/dist/src/managed-session.js +2 -18
- package/dist/src/managed-session.js.map +1 -1
- package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +13 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js +578 -2
- package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-command-adapter.js +325 -35
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +27 -0
- package/dist/src/openclaw-plugin-helpers.js +682 -2
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +53 -0
- package/dist/src/openclaw-plugin-schemas.js +53 -0
- package/dist/src/openclaw-plugin-schemas.js.map +1 -1
- package/dist/src/terminal-action-projection.d.ts +83 -1
- package/dist/src/terminal-action-projection.js +174 -0
- package/dist/src/terminal-action-projection.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +7 -0
- package/dist/src/terminal-agent-adapter.js +8 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +29 -0
- package/dist/src/terminal-agent-bridge.js +580 -14
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-control-ref.d.ts +23 -0
- package/dist/src/terminal-control-ref.js +32 -0
- package/dist/src/terminal-control-ref.js.map +1 -1
- package/dist/src/terminal-list-cli-adapter.js +324 -16
- package/dist/src/terminal-list-cli-adapter.js.map +1 -1
- package/dist/src/terminal-list-renderer.d.ts +1 -0
- package/dist/src/terminal-list-renderer.js +87 -3
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-model-control.d.ts +312 -0
- package/dist/src/terminal-model-control.js +1595 -0
- package/dist/src/terminal-model-control.js.map +1 -0
- package/dist/src/terminal-runtime-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-runtime-cli-adapter.js +59 -17
- package/dist/src/terminal-runtime-cli-adapter.js.map +1 -1
- package/docs/quickstart-herdr.md +1 -1
- package/docs/quickstart-tmux.md +27 -2
- package/openclaw.plugin.json +13 -1
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +54 -11
|
@@ -12,6 +12,7 @@ import { isSessionSendBlockingStatus, turnIdForConversation } from "./protocol.j
|
|
|
12
12
|
import { listManagedSessions, loadNativeThreadTransition } from "./session-store.js";
|
|
13
13
|
import { isExactClaudeNativeInspectionIdleComposer, NativeInspectionSubmissionError } from "./terminal-agent-bridge.js";
|
|
14
14
|
import { terminalControlAliasMatches, selectRootTerminalProcesses, withCodexCompanionFences } from "./terminal-authority-policy.js";
|
|
15
|
+
import { isTerminalModelReasoningEffort, terminalUserExplicitModelControlBindingToken, terminalUserExplicitModelControlResidualEntryBindingToken, terminalUserExplicitModelControlRepairBindingToken } from "./terminal-model-control.js";
|
|
15
16
|
import { nonBlankString } from "./value-guards.js";
|
|
16
17
|
export const CODEX_FOREGROUND_IDENTIFICATION_TTL_MS = 30_000;
|
|
17
18
|
export const CODEX_FOREGROUND_IDENTIFICATION_SCROLLBACK_LINES = 240;
|
|
@@ -26,6 +27,9 @@ export function createNativeThreadLifecycleCliAdapter(ports) {
|
|
|
26
27
|
agentAdapter: (options, agent) => app.agentAdapter(options, agent),
|
|
27
28
|
runList: (options) => app.runList(options),
|
|
28
29
|
runInspect: (options) => app.runInspect(options),
|
|
30
|
+
runModelOptions: (options) => app.runModelOptions(options),
|
|
31
|
+
runSetModel: (options) => app.runSetModel(options),
|
|
32
|
+
runRepairModelControl: (options) => app.runRepairModelControl(options),
|
|
29
33
|
runIdentifyForeground: (options) => app.runIdentifyForeground(options),
|
|
30
34
|
identifyCodexForegroundWhileLocked: (input) => app.identifyCodexForegroundWhileLocked(input),
|
|
31
35
|
assertSameInspectionTerminal: (expected, actual, stage) => app.assertSameInspectionTerminal(expected, actual, stage),
|
|
@@ -216,6 +220,52 @@ class NativeThreadLifecycleCliApplication {
|
|
|
216
220
|
bindingToken: bindingTokens[0], bindingTokens
|
|
217
221
|
});
|
|
218
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Model control on Codex is an explicit physical-pane operation. A freshly
|
|
225
|
+
* started TUI may legitimately have no rollout-backed Session yet. Keep the
|
|
226
|
+
* physical token separate from native lifecycle binding while retaining the
|
|
227
|
+
* resolver's fail-closed behavior for genuinely ambiguous live rollouts.
|
|
228
|
+
*/
|
|
229
|
+
async modelControlSnapshot(options, terminal) {
|
|
230
|
+
if (terminal.agent !== "codex") {
|
|
231
|
+
return this.currentSnapshot(options, terminal);
|
|
232
|
+
}
|
|
233
|
+
const snapshot = await this.currentSnapshot(options, terminal);
|
|
234
|
+
const nativeThreadId = snapshot.identity?.sessionId ??
|
|
235
|
+
snapshot.session?.binding?.native_thread_id;
|
|
236
|
+
if (isExactNativeThreadId(nativeThreadId) ||
|
|
237
|
+
snapshot.identity || snapshot.session) {
|
|
238
|
+
return snapshot;
|
|
239
|
+
}
|
|
240
|
+
const adapter = this.agentAdapter(options, terminal.agent);
|
|
241
|
+
const capability = adapter.probeModelControl?.(snapshot.version);
|
|
242
|
+
const plan = capability?.status === "supported"
|
|
243
|
+
? adapter.planModelControl?.(capability)
|
|
244
|
+
: undefined;
|
|
245
|
+
if (!plan || plan.behaviorProfile !== "codex-model-control-0.154.0") {
|
|
246
|
+
throw new Error(capability?.reason ?? "Codex has no verified physical model-control profile");
|
|
247
|
+
}
|
|
248
|
+
const bindingToken = this.physicalModelControlBindingToken(terminal, snapshot.version, plan.behaviorProfile);
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
...snapshot,
|
|
251
|
+
bindingToken,
|
|
252
|
+
bindingTokens: Object.freeze([bindingToken])
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
physicalModelControlBindingToken(terminal, agentVersion, behaviorProfile) {
|
|
256
|
+
const version = required(nonBlankString(agentVersion), "Codex physical model control requires an exact running version");
|
|
257
|
+
const incarnation = this.ports.identity.physicalProcessIncarnation(terminal.pid);
|
|
258
|
+
return terminalUserExplicitModelControlBindingToken({
|
|
259
|
+
terminalId: terminal.conversationId,
|
|
260
|
+
terminalControl: terminal.terminalControl,
|
|
261
|
+
pid: terminal.pid,
|
|
262
|
+
workspace: terminal.terminalControl.currentPath ?? this.ports.output.cwd(),
|
|
263
|
+
processUuid: incarnation.processUuid,
|
|
264
|
+
processBirth: incarnation.processBirth,
|
|
265
|
+
agentVersion: version,
|
|
266
|
+
behaviorProfile
|
|
267
|
+
});
|
|
268
|
+
}
|
|
219
269
|
async runList(options) {
|
|
220
270
|
const terminal = await this.resolveLifecycleTerminal(options);
|
|
221
271
|
const snapshot = await this.currentSnapshot(options, terminal);
|
|
@@ -537,7 +587,7 @@ class NativeThreadLifecycleCliApplication {
|
|
|
537
587
|
const terminal = await bridge.resolveStoredTerminal("codex", input.terminal.pid, input.terminal.terminalControl, physicalRuntime);
|
|
538
588
|
this.assertSameInspectionTerminal(input.terminal, terminal, "while waiting for foreground-identification control");
|
|
539
589
|
const realCwd = this.realForegroundWorkspace(terminal);
|
|
540
|
-
const processIncarnation = this.ports.identity.
|
|
590
|
+
const processIncarnation = this.ports.identity.physicalProcessIncarnation(terminal.pid);
|
|
541
591
|
this.assertForegroundPhysicalToken({
|
|
542
592
|
terminal,
|
|
543
593
|
processIncarnation,
|
|
@@ -659,7 +709,7 @@ class NativeThreadLifecycleCliApplication {
|
|
|
659
709
|
if (this.realForegroundWorkspace(input.terminal) !== boundary.realCwd) {
|
|
660
710
|
throw new Error(`terminal real cwd changed ${input.stage}; refresh AKK list`);
|
|
661
711
|
}
|
|
662
|
-
const processIncarnation = this.ports.identity.
|
|
712
|
+
const processIncarnation = this.ports.identity.physicalProcessIncarnation(input.terminal.pid);
|
|
663
713
|
if (processIncarnation.processUuid !== boundary.processUuid ||
|
|
664
714
|
processIncarnation.processBirth !== boundary.processBirth) {
|
|
665
715
|
throw new Error(`Codex process incarnation changed ${input.stage}; refresh AKK list`);
|
|
@@ -735,6 +785,524 @@ class NativeThreadLifecycleCliApplication {
|
|
|
735
785
|
"native-thread transition");
|
|
736
786
|
}
|
|
737
787
|
}
|
|
788
|
+
validateModelOptions(options) {
|
|
789
|
+
if (options.command !== undefined || options.message !== undefined ||
|
|
790
|
+
options.model !== undefined || options.reasoningEffort !== undefined ||
|
|
791
|
+
options.expectedCatalogFingerprint !== undefined) {
|
|
792
|
+
throw new Error("model-options accepts only an exact terminal and current binding authority");
|
|
793
|
+
}
|
|
794
|
+
return required(nonBlankString(options.expectedBindingToken), "--expected-binding-token is required");
|
|
795
|
+
}
|
|
796
|
+
validateSetModel(options) {
|
|
797
|
+
if (options.command !== undefined || options.message !== undefined ||
|
|
798
|
+
options.scope !== undefined || options.keys !== undefined ||
|
|
799
|
+
options.index !== undefined) {
|
|
800
|
+
throw new Error("set-model accepts no command, key, menu index, or caller-selected scope");
|
|
801
|
+
}
|
|
802
|
+
const expectedBindingToken = required(nonBlankString(options.expectedBindingToken), "--expected-binding-token is required");
|
|
803
|
+
const expectedCatalogFingerprint = required(nonBlankString(options.expectedCatalogFingerprint), "--expected-catalog-fingerprint is required");
|
|
804
|
+
if (!/^[0-9a-f]{64}$/u.test(expectedCatalogFingerprint)) {
|
|
805
|
+
throw new Error("--expected-catalog-fingerprint must be the exact sha256 from model-options");
|
|
806
|
+
}
|
|
807
|
+
const model = required(nonBlankString(options.model), "--model is required");
|
|
808
|
+
const reasoningEffort = options.reasoningEffort;
|
|
809
|
+
if (!isTerminalModelReasoningEffort(reasoningEffort)) {
|
|
810
|
+
throw new Error("--reasoning-effort must be one exact semantic value advertised by model-options");
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
expectedBindingToken,
|
|
814
|
+
expectedCatalogFingerprint,
|
|
815
|
+
request: { model, reasoningEffort }
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
validateRepairModelControl(options) {
|
|
819
|
+
if (options.command !== undefined ||
|
|
820
|
+
options.message !== undefined ||
|
|
821
|
+
options.model !== undefined ||
|
|
822
|
+
options.reasoningEffort !== undefined ||
|
|
823
|
+
options.expectedCatalogFingerprint !== undefined ||
|
|
824
|
+
options.scope !== undefined ||
|
|
825
|
+
options.keys !== undefined ||
|
|
826
|
+
options.index !== undefined) {
|
|
827
|
+
throw new Error("repair-model-control accepts only an exact terminal and current private repair authority");
|
|
828
|
+
}
|
|
829
|
+
return required(nonBlankString(options.expectedBindingToken), "--expected-binding-token is required");
|
|
830
|
+
}
|
|
831
|
+
async runModelOptions(options) {
|
|
832
|
+
const expectedBindingToken = this.validateModelOptions(options);
|
|
833
|
+
const context = await this.withPreparedModelControl(options, expectedBindingToken, async (prepared) => {
|
|
834
|
+
const result = await prepared.bridge.modelOptions(prepared.terminal.agent, prepared.terminal.terminalControl, prepared.agentVersion, prepared.plan, {
|
|
835
|
+
runtime: prepared.runtime,
|
|
836
|
+
beforeInput: () => prepared.initialResidual
|
|
837
|
+
? this.assertFreshModelControlResidualBoundary(prepared)
|
|
838
|
+
: this.assertFreshModelControlBoundary(prepared),
|
|
839
|
+
loadCodexCatalog: prepared.loadCodexCatalog,
|
|
840
|
+
initialResidual: prepared.initialResidual
|
|
841
|
+
});
|
|
842
|
+
if (prepared.initialResidual) {
|
|
843
|
+
await this.assertFreshModelControlBoundary({
|
|
844
|
+
...prepared,
|
|
845
|
+
expectedBindingToken: prepared.ordinaryBindingToken,
|
|
846
|
+
initialResidual: undefined
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return { prepared, catalog: result.catalog };
|
|
850
|
+
}, { allowResidualEntry: true });
|
|
851
|
+
this.ports.output.print(this.modelOptionsResult(context.prepared, context.catalog));
|
|
852
|
+
}
|
|
853
|
+
async runSetModel(options) {
|
|
854
|
+
const validated = this.validateSetModel(options);
|
|
855
|
+
const output = await this.withPreparedModelControl(options, validated.expectedBindingToken, async (prepared) => {
|
|
856
|
+
const result = await prepared.bridge.setModel(prepared.terminal.agent, prepared.terminal.terminalControl, prepared.agentVersion, prepared.plan, validated.expectedCatalogFingerprint, validated.request, {
|
|
857
|
+
runtime: prepared.runtime,
|
|
858
|
+
beforeInput: () => this.assertFreshModelControlBoundary(prepared),
|
|
859
|
+
loadCodexCatalog: prepared.loadCodexCatalog
|
|
860
|
+
});
|
|
861
|
+
return { prepared, result };
|
|
862
|
+
});
|
|
863
|
+
const { result, prepared } = output;
|
|
864
|
+
this.ports.output.print({
|
|
865
|
+
terminal_id: prepared.terminal.conversationId,
|
|
866
|
+
agent: prepared.terminal.agent,
|
|
867
|
+
agent_version: prepared.agentVersion,
|
|
868
|
+
behavior_profile: prepared.plan.behaviorProfile,
|
|
869
|
+
outcome: result.outcome,
|
|
870
|
+
requested: modelValueOutput(result.requested),
|
|
871
|
+
...(result.effective
|
|
872
|
+
? { effective: modelValueOutput(result.effective) }
|
|
873
|
+
: {}),
|
|
874
|
+
...(result.newSessionDefaults
|
|
875
|
+
? { new_session_defaults: modelValueOutput(result.newSessionDefaults) }
|
|
876
|
+
: {}),
|
|
877
|
+
scope: result.scope,
|
|
878
|
+
defaults_changed: result.defaultsChanged,
|
|
879
|
+
do_not_retry: result.doNotRetry,
|
|
880
|
+
...(result.reason ? { reason: result.reason } : {})
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
async runRepairModelControl(options) {
|
|
884
|
+
const expectedBindingToken = this.validateRepairModelControl(options);
|
|
885
|
+
const output = await this.withPreparedModelControlRepair(options, expectedBindingToken, async (prepared) => {
|
|
886
|
+
const result = await prepared.bridge.repairModelControlResidual(prepared.terminal.agent, prepared.terminal.terminalControl, prepared.agentVersion, prepared.plan, prepared.residual.fingerprint, {
|
|
887
|
+
runtime: prepared.runtime,
|
|
888
|
+
beforeInput: () => this.assertFreshModelControlResidualBoundary(prepared)
|
|
889
|
+
});
|
|
890
|
+
return { prepared, result };
|
|
891
|
+
});
|
|
892
|
+
this.ports.output.print({
|
|
893
|
+
terminal_id: output.prepared.terminal.conversationId,
|
|
894
|
+
agent: output.prepared.terminal.agent,
|
|
895
|
+
agent_version: output.prepared.agentVersion,
|
|
896
|
+
behavior_profile: output.prepared.plan.behaviorProfile,
|
|
897
|
+
outcome: output.result.outcome,
|
|
898
|
+
terminal_input_attempted: output.result.terminalInputAttempted,
|
|
899
|
+
composer_postcondition: output.result.composerPostcondition,
|
|
900
|
+
do_not_retry: output.result.doNotRetry,
|
|
901
|
+
...(output.result.reason ? { reason: output.result.reason } : {}),
|
|
902
|
+
...(output.result.outcome === "repaired"
|
|
903
|
+
? { next: "refresh_list_then_model_options" }
|
|
904
|
+
: {})
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
async withPreparedModelControlRepair(options, expectedBindingToken, operation) {
|
|
908
|
+
const storeDir = this.ports.state.storeDir(options);
|
|
909
|
+
if (this.ports.state.inspectStore(storeDir).writable !== true) {
|
|
910
|
+
throw new Error("model-control repair requires a compatible AKK Store for current-snapshot authority");
|
|
911
|
+
}
|
|
912
|
+
const initiallyResolved = await this.resolveLifecycleTerminal(options);
|
|
913
|
+
const release = this.ports.state.acquireTerminal(storeDir, initiallyResolved.terminalControl, { timeoutMs: 30_000 });
|
|
914
|
+
try {
|
|
915
|
+
const runtimeFacade = this.ports.runtime.forOptions(options);
|
|
916
|
+
const bridge = runtimeFacade.createBridge();
|
|
917
|
+
const terminal = await bridge.resolveStoredTerminal(initiallyResolved.agent, initiallyResolved.pid, initiallyResolved.terminalControl, { pid: initiallyResolved.pid });
|
|
918
|
+
this.assertSameInspectionTerminal(initiallyResolved, terminal, "while waiting for model-control repair ownership");
|
|
919
|
+
if (terminal.agent !== "codex") {
|
|
920
|
+
throw new Error("model-control residual repair currently supports only Codex 0.154.0");
|
|
921
|
+
}
|
|
922
|
+
const snapshot = await this.modelControlSnapshot(options, terminal);
|
|
923
|
+
const agentVersion = required(nonBlankString(snapshot.version), "model-control repair requires an exact running agent version");
|
|
924
|
+
const adapter = runtimeFacade.createAgentRegistry().require(terminal.agent);
|
|
925
|
+
const capability = adapter.probeModelControl?.(agentVersion);
|
|
926
|
+
const plan = capability?.status === "supported"
|
|
927
|
+
? adapter.planModelControl?.(capability)
|
|
928
|
+
: undefined;
|
|
929
|
+
if (!plan || plan.behaviorProfile !== "codex-model-control-0.154.0") {
|
|
930
|
+
throw new Error(capability?.reason ??
|
|
931
|
+
"the running Codex version has no verified model-control repair profile");
|
|
932
|
+
}
|
|
933
|
+
await this.assertModelControlExclusive({ options, terminal, snapshot });
|
|
934
|
+
const runtime = this.inspectionRuntime(terminal, snapshot);
|
|
935
|
+
await this.assertModelControlRepairReady({
|
|
936
|
+
options,
|
|
937
|
+
terminal,
|
|
938
|
+
snapshot,
|
|
939
|
+
bridge,
|
|
940
|
+
runtime
|
|
941
|
+
});
|
|
942
|
+
const residual = await bridge.inspectModelControlResidual(terminal.agent, terminal.terminalControl, agentVersion, plan, { runtime, beforeInput: () => undefined });
|
|
943
|
+
if (residual.state !== "recoverable") {
|
|
944
|
+
throw new Error(residual.reason);
|
|
945
|
+
}
|
|
946
|
+
const actualBindingToken = this.modelControlRepairBindingToken({
|
|
947
|
+
terminal,
|
|
948
|
+
agentVersion,
|
|
949
|
+
plan,
|
|
950
|
+
residual
|
|
951
|
+
});
|
|
952
|
+
if (actualBindingToken !== expectedBindingToken) {
|
|
953
|
+
throw new Error("the exact model-control residual changed after it was listed; refresh AKK list");
|
|
954
|
+
}
|
|
955
|
+
return await operation({
|
|
956
|
+
options,
|
|
957
|
+
terminal,
|
|
958
|
+
snapshot,
|
|
959
|
+
bridge,
|
|
960
|
+
runtime,
|
|
961
|
+
adapter,
|
|
962
|
+
agentVersion,
|
|
963
|
+
plan,
|
|
964
|
+
expectedBindingToken,
|
|
965
|
+
residual
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
finally {
|
|
969
|
+
release();
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async assertFreshModelControlResidualBoundary(context) {
|
|
973
|
+
const terminal = await this.resolveModelControlBoundaryTerminal(context);
|
|
974
|
+
this.assertSameInspectionTerminal(context.terminal, terminal, "immediately before model-control repair input");
|
|
975
|
+
const snapshot = await this.modelControlSnapshot(context.options, terminal);
|
|
976
|
+
if (snapshot.version !== context.agentVersion) {
|
|
977
|
+
throw new Error("terminal binding or coding-agent version changed during model-control repair");
|
|
978
|
+
}
|
|
979
|
+
const expectedNativeThreadId = context.snapshot.identity?.sessionId ??
|
|
980
|
+
context.snapshot.session?.binding?.native_thread_id;
|
|
981
|
+
const actualNativeThreadId = snapshot.identity?.sessionId ??
|
|
982
|
+
snapshot.session?.binding?.native_thread_id;
|
|
983
|
+
if (expectedNativeThreadId !== actualNativeThreadId) {
|
|
984
|
+
throw new Error("the current native Session changed during model-control repair");
|
|
985
|
+
}
|
|
986
|
+
const capability = context.adapter.probeModelControl?.(snapshot.version);
|
|
987
|
+
const plan = capability?.status === "supported"
|
|
988
|
+
? context.adapter.planModelControl?.(capability)
|
|
989
|
+
: undefined;
|
|
990
|
+
if (!plan || JSON.stringify(plan) !== JSON.stringify(context.plan)) {
|
|
991
|
+
throw new Error("the exact model-control repair profile changed during the operation");
|
|
992
|
+
}
|
|
993
|
+
await this.assertModelControlExclusive({
|
|
994
|
+
options: context.options,
|
|
995
|
+
terminal,
|
|
996
|
+
snapshot
|
|
997
|
+
});
|
|
998
|
+
await this.assertModelControlRepairReady({
|
|
999
|
+
options: context.options,
|
|
1000
|
+
terminal,
|
|
1001
|
+
snapshot,
|
|
1002
|
+
bridge: context.bridge,
|
|
1003
|
+
runtime: context.runtime
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
async assertModelControlRepairReady(input) {
|
|
1007
|
+
const status = await input.bridge.status(input.terminal.agent, input.terminal.terminalControl, { runtime: input.runtime });
|
|
1008
|
+
if (status.reachable !== true ||
|
|
1009
|
+
status.approval_state.scanned !== true ||
|
|
1010
|
+
status.approval_state.blocked === true ||
|
|
1011
|
+
status.interaction_state !== undefined) {
|
|
1012
|
+
throw new Error("model-control repair requires a reachable prompt with no approval or questionnaire");
|
|
1013
|
+
}
|
|
1014
|
+
const storeDir = this.ports.state.storeDir(input.options);
|
|
1015
|
+
const blocker = this.ports.state.terminalBlockingTurns(storeDir, input.terminal.terminalControl)[0];
|
|
1016
|
+
if (blocker) {
|
|
1017
|
+
throw new Error(`terminal ${input.terminal.terminalControl.target} still has unresolved Turn ` +
|
|
1018
|
+
`${turnIdForConversation(blocker)} (${blocker.status})`);
|
|
1019
|
+
}
|
|
1020
|
+
if (input.snapshot.session &&
|
|
1021
|
+
this.ports.state.hasUnresolvedTransition(storeDir, input.snapshot.session)) {
|
|
1022
|
+
throw new Error(`managed Session ${input.snapshot.session.session_id} has an unresolved native-thread transition`);
|
|
1023
|
+
}
|
|
1024
|
+
if (this.ports.state.dispatchOwnership(input.terminal.terminalControl).state !==
|
|
1025
|
+
"none") {
|
|
1026
|
+
throw new Error("the terminal has unresolved dispatch ownership during model-control repair");
|
|
1027
|
+
}
|
|
1028
|
+
if (this.ports.state.orphanedForRecovery(input.terminal.terminalControl)) {
|
|
1029
|
+
throw new Error("the terminal has unresolved input ownership during model-control repair");
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
modelControlRepairBindingToken(input) {
|
|
1033
|
+
const incarnation = this.ports.identity.physicalProcessIncarnation(input.terminal.pid);
|
|
1034
|
+
return terminalUserExplicitModelControlRepairBindingToken({
|
|
1035
|
+
terminalId: input.terminal.conversationId,
|
|
1036
|
+
terminalControl: input.terminal.terminalControl,
|
|
1037
|
+
pid: input.terminal.pid,
|
|
1038
|
+
workspace: required(nonBlankString(input.terminal.terminalControl.currentPath), "model-control repair requires an exact terminal cwd"),
|
|
1039
|
+
processUuid: incarnation.processUuid,
|
|
1040
|
+
processBirth: incarnation.processBirth,
|
|
1041
|
+
agentVersion: input.agentVersion,
|
|
1042
|
+
behaviorProfile: input.plan.behaviorProfile,
|
|
1043
|
+
residualKind: input.residual.kind,
|
|
1044
|
+
residualFingerprint: input.residual.fingerprint
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
modelControlResidualEntryBindingToken(input) {
|
|
1048
|
+
const incarnation = this.ports.identity.physicalProcessIncarnation(input.terminal.pid);
|
|
1049
|
+
return terminalUserExplicitModelControlResidualEntryBindingToken({
|
|
1050
|
+
terminalId: input.terminal.conversationId,
|
|
1051
|
+
terminalControl: input.terminal.terminalControl,
|
|
1052
|
+
pid: input.terminal.pid,
|
|
1053
|
+
workspace: required(nonBlankString(input.terminal.terminalControl.currentPath), "model-control continuation requires an exact terminal cwd"),
|
|
1054
|
+
processUuid: incarnation.processUuid,
|
|
1055
|
+
processBirth: incarnation.processBirth,
|
|
1056
|
+
agentVersion: input.agentVersion,
|
|
1057
|
+
behaviorProfile: input.plan.behaviorProfile,
|
|
1058
|
+
residualKind: input.residual.kind,
|
|
1059
|
+
residualFingerprint: input.residual.fingerprint
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
async withPreparedModelControl(options, expectedBindingToken, operation, behavior = {}) {
|
|
1063
|
+
const storeDir = this.ports.state.storeDir(options);
|
|
1064
|
+
if (this.ports.state.inspectStore(storeDir).writable !== true) {
|
|
1065
|
+
throw new Error("terminal model control requires a compatible AKK Store for current-snapshot authority");
|
|
1066
|
+
}
|
|
1067
|
+
const initiallyResolved = await this.resolveLifecycleTerminal(options);
|
|
1068
|
+
const release = this.ports.state.acquireTerminal(storeDir, initiallyResolved.terminalControl, { timeoutMs: 30_000 });
|
|
1069
|
+
try {
|
|
1070
|
+
const runtimeFacade = this.ports.runtime.forOptions(options);
|
|
1071
|
+
const bridge = runtimeFacade.createBridge();
|
|
1072
|
+
const terminal = await bridge.resolveStoredTerminal(initiallyResolved.agent, initiallyResolved.pid, initiallyResolved.terminalControl, { pid: initiallyResolved.pid });
|
|
1073
|
+
this.assertSameInspectionTerminal(initiallyResolved, terminal, "while waiting for model-control ownership");
|
|
1074
|
+
const snapshot = await this.modelControlSnapshot(options, terminal);
|
|
1075
|
+
const ordinaryBindingToken = required(nonBlankString(snapshot.bindingToken), "terminal model control requires current ordinary binding authority");
|
|
1076
|
+
const hasOrdinaryAuthority = snapshot.bindingTokens.includes(expectedBindingToken);
|
|
1077
|
+
if (!hasOrdinaryAuthority && !behavior.allowResidualEntry) {
|
|
1078
|
+
throw new Error("terminal binding changed after it was listed; refresh AKK list and retry");
|
|
1079
|
+
}
|
|
1080
|
+
const agentVersion = required(nonBlankString(snapshot.version), "model control requires an exact running agent version");
|
|
1081
|
+
const adapter = runtimeFacade.createAgentRegistry().require(terminal.agent);
|
|
1082
|
+
const capability = adapter.probeModelControl?.(agentVersion);
|
|
1083
|
+
if (capability?.status !== "supported" ||
|
|
1084
|
+
capability.modelSelection !== true ||
|
|
1085
|
+
capability.reasoningEffortSelection !== true) {
|
|
1086
|
+
throw new Error(capability?.reason ??
|
|
1087
|
+
`${adapter.displayName} has no verified model-control profile`);
|
|
1088
|
+
}
|
|
1089
|
+
const plan = adapter.planModelControl?.(capability);
|
|
1090
|
+
if (!plan) {
|
|
1091
|
+
throw new Error("the agent adapter did not produce a model-control plan");
|
|
1092
|
+
}
|
|
1093
|
+
await this.assertModelControlExclusive({ options, terminal, snapshot });
|
|
1094
|
+
let initialResidual;
|
|
1095
|
+
let runtime;
|
|
1096
|
+
if (hasOrdinaryAuthority) {
|
|
1097
|
+
runtime = await this.assertInitialInspectionReady(options, terminal, snapshot, bridge);
|
|
1098
|
+
const modelStatus = await bridge.status(terminal.agent, terminal.terminalControl, { runtime });
|
|
1099
|
+
if (modelStatus.approval_state.scanned !== true ||
|
|
1100
|
+
modelStatus.approval_state.blocked === true ||
|
|
1101
|
+
modelStatus.interaction_state !== undefined) {
|
|
1102
|
+
throw new Error("model control requires a freshly scanned terminal with no approval or questionnaire");
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
if (terminal.agent !== "codex" ||
|
|
1107
|
+
plan.behaviorProfile !== "codex-model-control-0.154.0") {
|
|
1108
|
+
throw new Error("terminal binding changed after it was listed; refresh AKK list and retry");
|
|
1109
|
+
}
|
|
1110
|
+
runtime = this.inspectionRuntime(terminal, snapshot);
|
|
1111
|
+
await this.assertModelControlRepairReady({
|
|
1112
|
+
options,
|
|
1113
|
+
terminal,
|
|
1114
|
+
snapshot,
|
|
1115
|
+
bridge,
|
|
1116
|
+
runtime
|
|
1117
|
+
});
|
|
1118
|
+
const observed = await bridge.inspectModelControlResidual(terminal.agent, terminal.terminalControl, agentVersion, plan, { runtime, beforeInput: () => undefined });
|
|
1119
|
+
if (observed.state !== "recoverable") {
|
|
1120
|
+
throw new Error(observed.reason);
|
|
1121
|
+
}
|
|
1122
|
+
const residualEntryToken = this.modelControlResidualEntryBindingToken({
|
|
1123
|
+
terminal,
|
|
1124
|
+
agentVersion,
|
|
1125
|
+
plan,
|
|
1126
|
+
residual: observed
|
|
1127
|
+
});
|
|
1128
|
+
if (residualEntryToken !== expectedBindingToken) {
|
|
1129
|
+
throw new Error("the exact model-control residual changed after it was listed; refresh AKK list");
|
|
1130
|
+
}
|
|
1131
|
+
initialResidual = observed;
|
|
1132
|
+
}
|
|
1133
|
+
return await operation({
|
|
1134
|
+
options,
|
|
1135
|
+
terminal,
|
|
1136
|
+
snapshot,
|
|
1137
|
+
bridge,
|
|
1138
|
+
runtime,
|
|
1139
|
+
adapter,
|
|
1140
|
+
agentVersion,
|
|
1141
|
+
plan,
|
|
1142
|
+
expectedBindingToken,
|
|
1143
|
+
ordinaryBindingToken,
|
|
1144
|
+
initialResidual,
|
|
1145
|
+
...(terminal.agent === "codex" &&
|
|
1146
|
+
plan.behaviorProfile === "codex-model-control-0.154.0"
|
|
1147
|
+
? {
|
|
1148
|
+
loadCodexCatalog: async () => runtimeFacade.codexModelCatalogForRunningProcess(terminal.pid, "0.154.0", terminal.terminalControl.currentPath)
|
|
1149
|
+
}
|
|
1150
|
+
: {})
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
finally {
|
|
1154
|
+
release();
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async assertFreshModelControlBoundary(context) {
|
|
1158
|
+
const terminal = await this.resolveModelControlBoundaryTerminal(context);
|
|
1159
|
+
this.assertSameInspectionTerminal(context.terminal, terminal, "immediately before model-control input");
|
|
1160
|
+
const snapshot = await this.modelControlSnapshot(context.options, terminal);
|
|
1161
|
+
if (!snapshot.bindingTokens.includes(context.expectedBindingToken) ||
|
|
1162
|
+
snapshot.version !== context.agentVersion) {
|
|
1163
|
+
throw new Error("terminal binding or coding-agent version changed during model control; refresh AKK list");
|
|
1164
|
+
}
|
|
1165
|
+
const expectedNativeThreadId = context.snapshot.identity?.sessionId ??
|
|
1166
|
+
context.snapshot.session?.binding?.native_thread_id;
|
|
1167
|
+
const actualNativeThreadId = snapshot.identity?.sessionId ??
|
|
1168
|
+
snapshot.session?.binding?.native_thread_id;
|
|
1169
|
+
if (expectedNativeThreadId !== actualNativeThreadId) {
|
|
1170
|
+
throw new Error("the current native Session changed during model control; inspect the pane before retrying");
|
|
1171
|
+
}
|
|
1172
|
+
const capability = context.adapter.probeModelControl?.(snapshot.version);
|
|
1173
|
+
const plan = capability?.status === "supported"
|
|
1174
|
+
? context.adapter.planModelControl?.(capability)
|
|
1175
|
+
: undefined;
|
|
1176
|
+
if (!plan || JSON.stringify(plan) !== JSON.stringify(context.plan)) {
|
|
1177
|
+
throw new Error("the exact model-control profile changed during the operation");
|
|
1178
|
+
}
|
|
1179
|
+
await this.assertModelControlExclusive({
|
|
1180
|
+
options: context.options,
|
|
1181
|
+
terminal,
|
|
1182
|
+
snapshot
|
|
1183
|
+
});
|
|
1184
|
+
this.assertInspectionReady({
|
|
1185
|
+
options: context.options,
|
|
1186
|
+
terminal,
|
|
1187
|
+
session: snapshot.session
|
|
1188
|
+
});
|
|
1189
|
+
this.assertModelControlAgentIdentity({
|
|
1190
|
+
options: context.options,
|
|
1191
|
+
terminal,
|
|
1192
|
+
snapshot
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
async resolveModelControlBoundaryTerminal(context) {
|
|
1196
|
+
try {
|
|
1197
|
+
return await context.bridge.resolveStoredTerminal(context.terminal.agent, context.terminal.pid, context.terminal.terminalControl, context.runtime);
|
|
1198
|
+
}
|
|
1199
|
+
catch (idleError) {
|
|
1200
|
+
if (context.terminal.agent !== "claude" ||
|
|
1201
|
+
context.plan.behaviorProfile !== "claude-model-control-2.1.266" ||
|
|
1202
|
+
context.runtime.requireExactClaudeAgentRow !== true ||
|
|
1203
|
+
context.runtime.exactClaudeAgentState !== "idle") {
|
|
1204
|
+
throw idleError;
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
return await context.bridge.resolveStoredTerminal(context.terminal.agent, context.terminal.pid, context.terminal.terminalControl, { ...context.runtime, exactClaudeAgentState: "status_dialog" });
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
// All immutable process, native Session, cwd, and terminal fences are
|
|
1211
|
+
// identical in both attempts. Only the exact Claude agents UI state
|
|
1212
|
+
// differs, and the bridge must still prove a fresh profiled model
|
|
1213
|
+
// surface after this read-only boundary before any key is dispatched.
|
|
1214
|
+
throw idleError;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
assertModelControlAgentIdentity(input) {
|
|
1219
|
+
if (input.terminal.agent !== "claude")
|
|
1220
|
+
return;
|
|
1221
|
+
try {
|
|
1222
|
+
this.assertInspectionAgentIdentity({
|
|
1223
|
+
...input,
|
|
1224
|
+
stage: "during model control",
|
|
1225
|
+
expectedClaudeState: "idle"
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
catch (idleError) {
|
|
1229
|
+
try {
|
|
1230
|
+
this.assertInspectionAgentIdentity({
|
|
1231
|
+
...input,
|
|
1232
|
+
stage: "during model control",
|
|
1233
|
+
expectedClaudeState: "status_dialog"
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
throw idleError;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async assertModelControlExclusive(input) {
|
|
1242
|
+
if (input.terminal.agent === "codex") {
|
|
1243
|
+
const nativeThreadId = input.snapshot.identity?.sessionId ??
|
|
1244
|
+
input.snapshot.session?.binding?.native_thread_id;
|
|
1245
|
+
if (!nativeThreadId && !input.snapshot.identity && !input.snapshot.session) {
|
|
1246
|
+
// A fresh, verified-empty Codex process has no durable native Session
|
|
1247
|
+
// to own yet. The physical pane/process/cwd fence remains exact.
|
|
1248
|
+
this.assertForegroundHasNoLifecycleTransition(input.options, input.terminal);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (!isExactNativeThreadId(nativeThreadId)) {
|
|
1252
|
+
throw new Error("Codex model control requires either a fresh zero-rollout pane or one exact current native Session identity");
|
|
1253
|
+
}
|
|
1254
|
+
await this.assertExclusive({
|
|
1255
|
+
options: input.options,
|
|
1256
|
+
agent: input.terminal.agent,
|
|
1257
|
+
currentPid: input.terminal.pid,
|
|
1258
|
+
nativeThreadId,
|
|
1259
|
+
storeDir: this.ports.state.storeDir(input.options),
|
|
1260
|
+
terminalControl: input.terminal.terminalControl,
|
|
1261
|
+
excludedManagedSessionId: input.snapshot.session?.session_id
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const nativeThreadId = input.snapshot.identity?.sessionId ??
|
|
1266
|
+
input.snapshot.session?.binding?.native_thread_id;
|
|
1267
|
+
if (!isExactNativeThreadId(nativeThreadId)) {
|
|
1268
|
+
throw new Error("model control requires one exact current native Session identity");
|
|
1269
|
+
}
|
|
1270
|
+
await this.assertExclusive({
|
|
1271
|
+
options: input.options,
|
|
1272
|
+
agent: input.terminal.agent,
|
|
1273
|
+
currentPid: input.terminal.pid,
|
|
1274
|
+
nativeThreadId,
|
|
1275
|
+
storeDir: this.ports.state.storeDir(input.options),
|
|
1276
|
+
terminalControl: input.terminal.terminalControl,
|
|
1277
|
+
excludedManagedSessionId: input.snapshot.session?.session_id
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
modelOptionsResult(context, catalog) {
|
|
1281
|
+
return {
|
|
1282
|
+
terminal_id: context.terminal.conversationId,
|
|
1283
|
+
agent: context.terminal.agent,
|
|
1284
|
+
agent_version: context.agentVersion,
|
|
1285
|
+
behavior_profile: catalog.behaviorProfile,
|
|
1286
|
+
scope: catalog.scope,
|
|
1287
|
+
current: modelValueOutput(catalog.current),
|
|
1288
|
+
models: catalog.models.map((model) => ({
|
|
1289
|
+
id: model.id,
|
|
1290
|
+
label: model.label,
|
|
1291
|
+
reasoning_efforts: [...model.reasoningEfforts]
|
|
1292
|
+
})),
|
|
1293
|
+
catalog_fingerprint: catalog.catalogFingerprint,
|
|
1294
|
+
available_actions: {
|
|
1295
|
+
set_model: {
|
|
1296
|
+
tool: "agent_knock_knock_set_model",
|
|
1297
|
+
arguments: {
|
|
1298
|
+
terminal_id: context.terminal.conversationId,
|
|
1299
|
+
expected_binding_token: context.ordinaryBindingToken,
|
|
1300
|
+
expected_catalog_fingerprint: catalog.catalogFingerprint
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
738
1306
|
validateInspectionOptions(options) {
|
|
739
1307
|
const inspection = required(nonBlankString(options.inspection), "--inspection is required");
|
|
740
1308
|
if (inspection !== "status") {
|
|
@@ -1020,6 +1588,14 @@ class NativeThreadLifecycleCliApplication {
|
|
|
1020
1588
|
});
|
|
1021
1589
|
}
|
|
1022
1590
|
}
|
|
1591
|
+
function modelValueOutput(value) {
|
|
1592
|
+
return {
|
|
1593
|
+
model: value.model,
|
|
1594
|
+
...(value.reasoningEffort
|
|
1595
|
+
? { reasoning_effort: value.reasoningEffort }
|
|
1596
|
+
: {})
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1023
1599
|
function bareNativeInspectionScreenDigest(fingerprint) {
|
|
1024
1600
|
return /^sha256:([0-9a-f]{64})$/u.exec(fingerprint)?.[1];
|
|
1025
1601
|
}
|