larkin 0.3.3 → 0.4.2

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.
Files changed (32) hide show
  1. package/README.md +72 -33
  2. package/assets/readme/larkin-hero.svg +111 -0
  3. package/dist/agent/agent-state-store.cjs +321 -22
  4. package/dist/agent/agent-state-store.mjs +320 -22
  5. package/dist/agent/agent-transport.cjs +6 -4
  6. package/dist/agent/agent-transport.mjs +6 -4
  7. package/dist/agent/host-reminder-orchestrator.cjs +9 -3
  8. package/dist/agent/host-reminder-orchestrator.mjs +9 -3
  9. package/dist/agent/inbox-projection.cjs +114 -23
  10. package/dist/agent/inbox-projection.mjs +112 -23
  11. package/dist/app/cli.mjs +3 -1
  12. package/dist/app/local-control.mjs +108 -2
  13. package/dist/app/runtime-process.mjs +4 -0
  14. package/dist/app/session-cli.mjs +55 -20
  15. package/dist/feishu/document-comment.cjs +2 -1
  16. package/dist/feishu/document-comment.mjs +2 -1
  17. package/dist/feishu/host-business-state.cjs +7 -2
  18. package/dist/feishu/host-business-state.mjs +7 -2
  19. package/dist/feishu/host-shell.cjs +138 -32
  20. package/dist/feishu/host-shell.mjs +138 -32
  21. package/dist/runtime/pi-compaction-recovery.cjs +623 -0
  22. package/dist/runtime/pi-compaction-recovery.mjs +602 -0
  23. package/dist/runtime/pi-provider-config.cjs +2 -2
  24. package/dist/runtime/pi-provider-config.mjs +2 -2
  25. package/dist/runtime/pi-rpc-client.cjs +39 -22
  26. package/dist/runtime/pi-rpc-client.mjs +39 -22
  27. package/dist/runtime/provider-error-classifier.cjs +39 -0
  28. package/dist/runtime/provider-error-classifier.mjs +34 -0
  29. package/dist/runtime/runtime-adapters.mjs +82 -19
  30. package/dist/runtime/runtime-host.cjs +663 -12
  31. package/dist/runtime/runtime-host.mjs +661 -11
  32. package/package.json +1 -1
@@ -1,22 +1,118 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RUNTIME_REDELIVERY_TARGET = exports.RUNTIME_REMINDER_TARGET = void 0;
4
+ exports.isCanonicalInboxTarget = isCanonicalInboxTarget;
3
5
  exports.targetKeyOfInboxEnvelope = targetKeyOfInboxEnvelope;
4
6
  exports.projectInboxEnvelope = projectInboxEnvelope;
5
7
  exports.targetOfInboxEnvelope = targetOfInboxEnvelope;
6
8
  exports.projectInboxEvents = projectInboxEvents;
7
9
  exports.projectInboxCheck = projectInboxCheck;
10
+ exports.RUNTIME_REMINDER_TARGET = "runtime:reminder";
11
+ exports.RUNTIME_REDELIVERY_TARGET = "runtime:redelivery";
12
+ const CHAT_TARGET_PREFIX = "chat:";
13
+ const THREAD_TARGET_PREFIX = "thread:";
14
+ const DOCUMENT_COMMENT_TARGET_PREFIX = "document-comment:";
15
+ const REMINDER_MESSAGE_PREFIX = "rem_";
16
+ const REDELIVERY_MESSAGE_PREFIX = "redeliver_";
17
+ function hasNonemptySuffix(value, prefix) {
18
+ return value.startsWith(prefix) && value.length > prefix.length;
19
+ }
20
+ function internalTargetOfInboxEnvelope(envelope) {
21
+ const messageId = typeof envelope.message_id === "string" ? envelope.message_id : "";
22
+ const reminderKind = envelope.kind === "reminder";
23
+ const redeliveryKind = envelope.kind === "redelivery";
24
+ const reminderMarker = messageId.startsWith(REMINDER_MESSAGE_PREFIX);
25
+ const redeliveryMarker = messageId.startsWith(REDELIVERY_MESSAGE_PREFIX);
26
+ const reminderId = hasNonemptySuffix(messageId, REMINDER_MESSAGE_PREFIX);
27
+ const redeliveryId = hasNonemptySuffix(messageId, REDELIVERY_MESSAGE_PREFIX);
28
+ const hasInternalMarker = reminderKind || redeliveryKind || reminderMarker || redeliveryMarker;
29
+ if (!hasInternalMarker)
30
+ return null;
31
+ if (reminderKind && reminderId && !redeliveryKind && !redeliveryMarker)
32
+ return exports.RUNTIME_REMINDER_TARGET;
33
+ if (redeliveryKind && redeliveryId && !reminderKind && !reminderMarker)
34
+ return exports.RUNTIME_REDELIVERY_TARGET;
35
+ throw new Error(`Inbox internal source requires matching kind and message_id prefix; received kind=${JSON.stringify(envelope.kind ?? null)} message_id=${JSON.stringify(envelope.message_id ?? null)}`);
36
+ }
37
+ function isChatTarget(target) {
38
+ return hasNonemptySuffix(target, CHAT_TARGET_PREFIX);
39
+ }
40
+ function isThreadTarget(target) {
41
+ return hasNonemptySuffix(target, THREAD_TARGET_PREFIX);
42
+ }
43
+ function isDocumentCommentTarget(target) {
44
+ return hasNonemptySuffix(target, DOCUMENT_COMMENT_TARGET_PREFIX);
45
+ }
46
+ function isCanonicalInboxTarget(target) {
47
+ return isChatTarget(target)
48
+ || isThreadTarget(target)
49
+ || target === exports.RUNTIME_REMINDER_TARGET
50
+ || target === exports.RUNTIME_REDELIVERY_TARGET
51
+ || isDocumentCommentTarget(target);
52
+ }
53
+ function invalidTarget(target) {
54
+ return new Error(`Invalid canonical Inbox target ${JSON.stringify(target)}; expected chat:<nonempty>, thread:<nonempty>, runtime:reminder, runtime:redelivery, or document-comment:<nonempty>`);
55
+ }
56
+ function optionalLocator(envelope, key) {
57
+ const value = envelope[key];
58
+ if (value === undefined || value === null || value === "")
59
+ return "";
60
+ if (typeof value !== "string")
61
+ throw new Error(`Inbox ${key} must be a string when present; received ${JSON.stringify(value)}`);
62
+ return value;
63
+ }
8
64
  function targetKeyOfInboxEnvelope(envelope) {
9
65
  if (!envelope)
10
- return "runtime:unknown";
11
- if (envelope.kind === "document_comment" && typeof envelope.target === "string" && envelope.target) {
12
- return envelope.target;
66
+ throw new Error("Inbox envelope is required to derive a canonical target");
67
+ const internalTarget = internalTargetOfInboxEnvelope(envelope);
68
+ const chatId = optionalLocator(envelope, "chat_id");
69
+ const threadId = optionalLocator(envelope, "thread_id");
70
+ if (threadId && !chatId) {
71
+ throw new Error(`Inbox thread locator requires nonempty chat_id and thread_id; received chat_id=${JSON.stringify(chatId)} thread_id=${JSON.stringify(threadId)}`);
13
72
  }
14
- if (typeof envelope.chat_id === "string" && envelope.chat_id) {
15
- return typeof envelope.thread_id === "string" && envelope.thread_id
16
- ? `thread:${envelope.chat_id}:${envelope.thread_id}`
17
- : `chat:${envelope.chat_id}`;
73
+ const locatorTarget = chatId ? (threadId ? `thread:${chatId}:${threadId}` : `chat:${chatId}`) : null;
74
+ const hasExplicitTarget = Object.prototype.hasOwnProperty.call(envelope, "target");
75
+ if (hasExplicitTarget) {
76
+ if (typeof envelope.target !== "string" || envelope.target.length === 0)
77
+ throw invalidTarget(String(envelope.target));
78
+ const target = envelope.target;
79
+ if (!isCanonicalInboxTarget(target))
80
+ throw invalidTarget(target);
81
+ if (isChatTarget(target) || isThreadTarget(target)) {
82
+ if (internalTarget || envelope.kind === "document_comment")
83
+ throw new Error(`Inbox chat/thread target conflicts with source kind/message_id`);
84
+ if (locatorTarget && locatorTarget !== target) {
85
+ throw new Error(`Inbox chat/thread target conflicts with locator target ${JSON.stringify(locatorTarget)}`);
86
+ }
87
+ return target;
88
+ }
89
+ if (target === exports.RUNTIME_REMINDER_TARGET || target === exports.RUNTIME_REDELIVERY_TARGET) {
90
+ if (locatorTarget)
91
+ throw new Error(`Inbox runtime target cannot carry chat_id/thread_id locators`);
92
+ if (internalTarget !== target)
93
+ throw new Error(`Inbox target ${target} requires exact matching kind and message_id prefix`);
94
+ return target;
95
+ }
96
+ if (isDocumentCommentTarget(target)) {
97
+ if (envelope.kind !== "document_comment" || internalTarget)
98
+ throw new Error(`Inbox document-comment target requires kind=document_comment and no internal source`);
99
+ if (locatorTarget)
100
+ throw new Error(`Inbox document-comment target cannot carry chat_id/thread_id locators`);
101
+ return target;
102
+ }
103
+ throw invalidTarget(target);
18
104
  }
19
- return targetOfInboxEnvelope(envelope) || "runtime:system";
105
+ if (envelope.kind === "document_comment") {
106
+ throw new Error("Inbox document_comment requires an explicit document-comment:<nonempty> target");
107
+ }
108
+ if (locatorTarget) {
109
+ if (internalTarget)
110
+ throw new Error(`Inbox internal source cannot be derived as a chat/thread target`);
111
+ return locatorTarget;
112
+ }
113
+ if (internalTarget)
114
+ throw new Error(`Inbox internal source requires an explicit ${internalTarget} target`);
115
+ throw new Error(`Inbox envelope has no canonical target locator (message_id=${JSON.stringify(envelope.message_id ?? null)}, kind=${JSON.stringify(envelope.kind ?? null)})`);
20
116
  }
21
117
  /** Enrich the durable local Inbox without changing the canonical Runtime envelope. */
22
118
  function projectInboxEnvelope(canonical, locators) {
@@ -32,22 +128,14 @@ function projectInboxEnvelope(canonical, locators) {
32
128
  content: locators.content !== undefined ? locators.content : (typeof canonical.content === "string" ? canonical.content : ""),
33
129
  };
34
130
  }
35
- /** Preserve the target/thread format consumed by the existing Agent CLI. */
131
+ /** Return an allowlisted canonical target when the envelope is locatable. */
36
132
  function targetOfInboxEnvelope(envelope) {
37
- if (!envelope)
38
- return null;
39
- if (envelope.kind === "document_comment") {
40
- return typeof envelope.target === "string" && envelope.target ? envelope.target : null;
133
+ try {
134
+ return targetKeyOfInboxEnvelope(envelope);
41
135
  }
42
- if (envelope.channel_type === "thread" && envelope.parent_channel_name) {
43
- const base = envelope.parent_channel_type === "dm"
44
- ? `dm:@${String(envelope.parent_channel_name)}`
45
- : `#${String(envelope.parent_channel_name)}`;
46
- return `${base}:${String(envelope.channel_name || "").slice(0, 8)}`;
136
+ catch {
137
+ return null;
47
138
  }
48
- return envelope.channel_type === "dm"
49
- ? `dm:@${String(envelope.channel_name)}`
50
- : `#${String(envelope.channel_name)}`;
51
139
  }
52
140
  /** Project persisted inbox envelopes into the exact agentApi events response data shape. */
53
141
  function projectInboxEvents(envelopes) {
@@ -56,7 +144,7 @@ function projectInboxEvents(envelopes) {
56
144
  events: envelopes,
57
145
  last_seen_msgId: last ? (last.message_id || null) : null,
58
146
  last_seen_seq: last ? (last.seq ?? null) : null,
59
- reply_target: targetOfInboxEnvelope(last),
147
+ reply_target: last ? targetKeyOfInboxEnvelope(last) : null,
60
148
  pending_notice_ids: [],
61
149
  wake_reason: null,
62
150
  has_more: false,
@@ -64,9 +152,12 @@ function projectInboxEvents(envelopes) {
64
152
  }
65
153
  /** Bounded, content-light and deterministic projection for the read-only check command. */
66
154
  function projectInboxCheck(envelopes, onlyTarget) {
155
+ if (onlyTarget && !isCanonicalInboxTarget(onlyTarget)) {
156
+ throw new Error(`Invalid canonical Inbox check target ${JSON.stringify(onlyTarget)}`);
157
+ }
67
158
  const byTarget = new Map();
68
159
  for (const envelope of envelopes) {
69
- const target = typeof envelope.target === "string" && envelope.target ? envelope.target : targetKeyOfInboxEnvelope(envelope);
160
+ const target = targetKeyOfInboxEnvelope(envelope);
70
161
  if (onlyTarget && onlyTarget !== target)
71
162
  continue;
72
163
  const rows = byTarget.get(target) ?? [];
@@ -1,15 +1,109 @@
1
+ export const RUNTIME_REMINDER_TARGET = "runtime:reminder";
2
+ export const RUNTIME_REDELIVERY_TARGET = "runtime:redelivery";
3
+ const CHAT_TARGET_PREFIX = "chat:";
4
+ const THREAD_TARGET_PREFIX = "thread:";
5
+ const DOCUMENT_COMMENT_TARGET_PREFIX = "document-comment:";
6
+ const REMINDER_MESSAGE_PREFIX = "rem_";
7
+ const REDELIVERY_MESSAGE_PREFIX = "redeliver_";
8
+ function hasNonemptySuffix(value, prefix) {
9
+ return value.startsWith(prefix) && value.length > prefix.length;
10
+ }
11
+ function internalTargetOfInboxEnvelope(envelope) {
12
+ const messageId = typeof envelope.message_id === "string" ? envelope.message_id : "";
13
+ const reminderKind = envelope.kind === "reminder";
14
+ const redeliveryKind = envelope.kind === "redelivery";
15
+ const reminderMarker = messageId.startsWith(REMINDER_MESSAGE_PREFIX);
16
+ const redeliveryMarker = messageId.startsWith(REDELIVERY_MESSAGE_PREFIX);
17
+ const reminderId = hasNonemptySuffix(messageId, REMINDER_MESSAGE_PREFIX);
18
+ const redeliveryId = hasNonemptySuffix(messageId, REDELIVERY_MESSAGE_PREFIX);
19
+ const hasInternalMarker = reminderKind || redeliveryKind || reminderMarker || redeliveryMarker;
20
+ if (!hasInternalMarker)
21
+ return null;
22
+ if (reminderKind && reminderId && !redeliveryKind && !redeliveryMarker)
23
+ return RUNTIME_REMINDER_TARGET;
24
+ if (redeliveryKind && redeliveryId && !reminderKind && !reminderMarker)
25
+ return RUNTIME_REDELIVERY_TARGET;
26
+ throw new Error(`Inbox internal source requires matching kind and message_id prefix; received kind=${JSON.stringify(envelope.kind ?? null)} message_id=${JSON.stringify(envelope.message_id ?? null)}`);
27
+ }
28
+ function isChatTarget(target) {
29
+ return hasNonemptySuffix(target, CHAT_TARGET_PREFIX);
30
+ }
31
+ function isThreadTarget(target) {
32
+ return hasNonemptySuffix(target, THREAD_TARGET_PREFIX);
33
+ }
34
+ function isDocumentCommentTarget(target) {
35
+ return hasNonemptySuffix(target, DOCUMENT_COMMENT_TARGET_PREFIX);
36
+ }
37
+ export function isCanonicalInboxTarget(target) {
38
+ return isChatTarget(target)
39
+ || isThreadTarget(target)
40
+ || target === RUNTIME_REMINDER_TARGET
41
+ || target === RUNTIME_REDELIVERY_TARGET
42
+ || isDocumentCommentTarget(target);
43
+ }
44
+ function invalidTarget(target) {
45
+ return new Error(`Invalid canonical Inbox target ${JSON.stringify(target)}; expected chat:<nonempty>, thread:<nonempty>, runtime:reminder, runtime:redelivery, or document-comment:<nonempty>`);
46
+ }
47
+ function optionalLocator(envelope, key) {
48
+ const value = envelope[key];
49
+ if (value === undefined || value === null || value === "")
50
+ return "";
51
+ if (typeof value !== "string")
52
+ throw new Error(`Inbox ${key} must be a string when present; received ${JSON.stringify(value)}`);
53
+ return value;
54
+ }
1
55
  export function targetKeyOfInboxEnvelope(envelope) {
2
56
  if (!envelope)
3
- return "runtime:unknown";
4
- if (envelope.kind === "document_comment" && typeof envelope.target === "string" && envelope.target) {
5
- return envelope.target;
57
+ throw new Error("Inbox envelope is required to derive a canonical target");
58
+ const internalTarget = internalTargetOfInboxEnvelope(envelope);
59
+ const chatId = optionalLocator(envelope, "chat_id");
60
+ const threadId = optionalLocator(envelope, "thread_id");
61
+ if (threadId && !chatId) {
62
+ throw new Error(`Inbox thread locator requires nonempty chat_id and thread_id; received chat_id=${JSON.stringify(chatId)} thread_id=${JSON.stringify(threadId)}`);
6
63
  }
7
- if (typeof envelope.chat_id === "string" && envelope.chat_id) {
8
- return typeof envelope.thread_id === "string" && envelope.thread_id
9
- ? `thread:${envelope.chat_id}:${envelope.thread_id}`
10
- : `chat:${envelope.chat_id}`;
64
+ const locatorTarget = chatId ? (threadId ? `thread:${chatId}:${threadId}` : `chat:${chatId}`) : null;
65
+ const hasExplicitTarget = Object.prototype.hasOwnProperty.call(envelope, "target");
66
+ if (hasExplicitTarget) {
67
+ if (typeof envelope.target !== "string" || envelope.target.length === 0)
68
+ throw invalidTarget(String(envelope.target));
69
+ const target = envelope.target;
70
+ if (!isCanonicalInboxTarget(target))
71
+ throw invalidTarget(target);
72
+ if (isChatTarget(target) || isThreadTarget(target)) {
73
+ if (internalTarget || envelope.kind === "document_comment")
74
+ throw new Error(`Inbox chat/thread target conflicts with source kind/message_id`);
75
+ if (locatorTarget && locatorTarget !== target) {
76
+ throw new Error(`Inbox chat/thread target conflicts with locator target ${JSON.stringify(locatorTarget)}`);
77
+ }
78
+ return target;
79
+ }
80
+ if (target === RUNTIME_REMINDER_TARGET || target === RUNTIME_REDELIVERY_TARGET) {
81
+ if (locatorTarget)
82
+ throw new Error(`Inbox runtime target cannot carry chat_id/thread_id locators`);
83
+ if (internalTarget !== target)
84
+ throw new Error(`Inbox target ${target} requires exact matching kind and message_id prefix`);
85
+ return target;
86
+ }
87
+ if (isDocumentCommentTarget(target)) {
88
+ if (envelope.kind !== "document_comment" || internalTarget)
89
+ throw new Error(`Inbox document-comment target requires kind=document_comment and no internal source`);
90
+ if (locatorTarget)
91
+ throw new Error(`Inbox document-comment target cannot carry chat_id/thread_id locators`);
92
+ return target;
93
+ }
94
+ throw invalidTarget(target);
11
95
  }
12
- return targetOfInboxEnvelope(envelope) || "runtime:system";
96
+ if (envelope.kind === "document_comment") {
97
+ throw new Error("Inbox document_comment requires an explicit document-comment:<nonempty> target");
98
+ }
99
+ if (locatorTarget) {
100
+ if (internalTarget)
101
+ throw new Error(`Inbox internal source cannot be derived as a chat/thread target`);
102
+ return locatorTarget;
103
+ }
104
+ if (internalTarget)
105
+ throw new Error(`Inbox internal source requires an explicit ${internalTarget} target`);
106
+ throw new Error(`Inbox envelope has no canonical target locator (message_id=${JSON.stringify(envelope.message_id ?? null)}, kind=${JSON.stringify(envelope.kind ?? null)})`);
13
107
  }
14
108
  /** Enrich the durable local Inbox without changing the canonical Runtime envelope. */
15
109
  export function projectInboxEnvelope(canonical, locators) {
@@ -25,22 +119,14 @@ export function projectInboxEnvelope(canonical, locators) {
25
119
  content: locators.content !== undefined ? locators.content : (typeof canonical.content === "string" ? canonical.content : ""),
26
120
  };
27
121
  }
28
- /** Preserve the target/thread format consumed by the existing Agent CLI. */
122
+ /** Return an allowlisted canonical target when the envelope is locatable. */
29
123
  export function targetOfInboxEnvelope(envelope) {
30
- if (!envelope)
31
- return null;
32
- if (envelope.kind === "document_comment") {
33
- return typeof envelope.target === "string" && envelope.target ? envelope.target : null;
124
+ try {
125
+ return targetKeyOfInboxEnvelope(envelope);
34
126
  }
35
- if (envelope.channel_type === "thread" && envelope.parent_channel_name) {
36
- const base = envelope.parent_channel_type === "dm"
37
- ? `dm:@${String(envelope.parent_channel_name)}`
38
- : `#${String(envelope.parent_channel_name)}`;
39
- return `${base}:${String(envelope.channel_name || "").slice(0, 8)}`;
127
+ catch {
128
+ return null;
40
129
  }
41
- return envelope.channel_type === "dm"
42
- ? `dm:@${String(envelope.channel_name)}`
43
- : `#${String(envelope.channel_name)}`;
44
130
  }
45
131
  /** Project persisted inbox envelopes into the exact agentApi events response data shape. */
46
132
  export function projectInboxEvents(envelopes) {
@@ -49,7 +135,7 @@ export function projectInboxEvents(envelopes) {
49
135
  events: envelopes,
50
136
  last_seen_msgId: last ? (last.message_id || null) : null,
51
137
  last_seen_seq: last ? (last.seq ?? null) : null,
52
- reply_target: targetOfInboxEnvelope(last),
138
+ reply_target: last ? targetKeyOfInboxEnvelope(last) : null,
53
139
  pending_notice_ids: [],
54
140
  wake_reason: null,
55
141
  has_more: false,
@@ -57,9 +143,12 @@ export function projectInboxEvents(envelopes) {
57
143
  }
58
144
  /** Bounded, content-light and deterministic projection for the read-only check command. */
59
145
  export function projectInboxCheck(envelopes, onlyTarget) {
146
+ if (onlyTarget && !isCanonicalInboxTarget(onlyTarget)) {
147
+ throw new Error(`Invalid canonical Inbox check target ${JSON.stringify(onlyTarget)}`);
148
+ }
60
149
  const byTarget = new Map();
61
150
  for (const envelope of envelopes) {
62
- const target = typeof envelope.target === "string" && envelope.target ? envelope.target : targetKeyOfInboxEnvelope(envelope);
151
+ const target = targetKeyOfInboxEnvelope(envelope);
63
152
  if (onlyTarget && onlyTarget !== target)
64
153
  continue;
65
154
  const rows = byTarget.get(target) ?? [];
package/dist/app/cli.mjs CHANGED
@@ -77,7 +77,8 @@ Examples:
77
77
 
78
78
  Credentials, internal paths, serverId, activeAgent, and raw config are never exposed here.`,
79
79
  session: `Usage: larkin session reset --agent <App ID> --json [--wait-ready <seconds>]
80
- Atomically replace one idle, zero-backlog Agent Runtime session through authenticated local control.`,
80
+ larkin session recover --agent <App ID> --reason context-overflow --json [--wait-ready <seconds>]
81
+ Reset replaces one idle, zero-backlog Runtime session. Recover is an explicit operator-only context-window recovery that preserves and replays canonical Inbox deliveries.`,
81
82
  "pi-auth": `Usage: larkin pi-auth status [--agent <App ID>] [--json]
82
83
  larkin pi-auth logout <provider> [--agent <App ID>]
83
84
  Show non-sensitive official Pi credential metadata or remove one target provider credential.`,
@@ -124,6 +125,7 @@ Usage: larkin <command>
124
125
  chats List known chats; use free/strict <oc_id> to configure mention requirements
125
126
  config Inspect effective config/source, edit mention inheritance, or explicitly apply runtime changes
126
127
  session reset Replace one idle, zero-backlog Agent Runtime session for a fresh scenario
128
+ session recover Explicitly recover a context-overflowed session and replay retained Inbox deliveries
127
129
  pi-auth Show non-sensitive built-in Pi auth status or logout one provider
128
130
  comment reply Reply to the exact cloud-document comment bound by a polled Inbox message
129
131
  telemetry Inspect, export, import, or flush the durable OpenTelemetry trace queue
@@ -15,6 +15,48 @@ const AGENT_ID = /^cli_[A-Za-z0-9]+$/;
15
15
  const OPERATION_ID = /^[A-Za-z0-9_-]{8,128}$/;
16
16
  const AUTHORIZATION = /^[A-Za-z0-9_-]{43,128}$/;
17
17
  const UNIX_SOCKET_PATH_MAX_BYTES = process.platform === "darwin" ? 103 : 107;
18
+ function recoveryErrorText(code) {
19
+ switch (code) {
20
+ case "unknown_agent": return "unknown Agent";
21
+ case "agent_busy": return "Agent is not idle";
22
+ case "channel_unavailable": return "Runtime channel is unavailable";
23
+ case "channel_reconnecting": return "Runtime channel is reconnecting";
24
+ case "runtime_unavailable": return "Runtime readiness is unavailable";
25
+ case "recovery_unavailable": return "context-overflow recovery is unavailable";
26
+ case "recovery_staged_not_committed": return "context-overflow recovery was staged but not committed";
27
+ case "state_persistence_failed": return "context-overflow recovery committed but state persistence failed";
28
+ case "recovery_timeout": return "context-overflow recovery committed but readiness did not converge";
29
+ case "recovery_refused": return "context-overflow recovery was refused";
30
+ default: return "context-overflow recovery failed";
31
+ }
32
+ }
33
+ function sanitizeRecoveryReadiness(value) {
34
+ if (!value || typeof value !== "object" || Array.isArray(value))
35
+ return undefined;
36
+ const candidate = value;
37
+ if (!["codex", "claude", "pi"].includes(String(candidate.runtime))
38
+ || !["missing", "unauthenticated", "unavailable", "incompatible", "ready"].includes(String(candidate.state)))
39
+ return undefined;
40
+ const runtime = candidate.runtime;
41
+ const state = candidate.state;
42
+ return { runtime, state,
43
+ ...(state === "ready" ? {} : { reason: `Runtime readiness is ${state}.`, nextAction: "Inspect Runtime/provider configuration, then retry." }) };
44
+ }
45
+ function sanitizeSessionRecoveryResponse(response, agentId) {
46
+ const code = typeof response.code === "string" && /^[a-z][a-z0-9_]{0,63}$/.test(response.code) ? response.code : undefined;
47
+ return {
48
+ ok: response.ok === true, agentId, ...(code ? { code } : {}),
49
+ ...(code || response.error ? { error: recoveryErrorText(code) } : {}),
50
+ recoveryCommitted: response.recoveryCommitted === true, generationChanged: response.generationChanged === true,
51
+ sessionChanged: response.sessionChanged === true, turns: 0, runtimeReady: response.runtimeReady === true,
52
+ channelConnected: response.channelConnected === true, reconnecting: response.reconnecting === true,
53
+ pendingCount: Math.max(0, Number(response.pendingCount) || 0), rearmedCount: Math.max(0, Number(response.rearmedCount) || 0),
54
+ replayStatus: ["scheduled", "pending", "consumed", "not_started"].includes(response.replayStatus) ? response.replayStatus : "not_started",
55
+ remainingPendingCount: Math.max(0, Number(response.remainingPendingCount) || 0),
56
+ readyForFreshScenario: response.readyForFreshScenario === true, inboundObserved: false,
57
+ ...(sanitizeRecoveryReadiness(response.readiness) ? { readiness: sanitizeRecoveryReadiness(response.readiness) } : {}),
58
+ };
59
+ }
18
60
  function socketPathsFit(root) {
19
61
  if (isWindows)
20
62
  return true; // Windows named-pipe socket paths use a much larger limit
@@ -92,7 +134,7 @@ function parseRequest(line) {
92
134
  throw new Error("invalid agent control request");
93
135
  }
94
136
  if (value.operation === "session-reset") {
95
- if (value.waitReadyMs !== undefined && (!Number.isSafeInteger(value.waitReadyMs) || value.waitReadyMs < 0 || value.waitReadyMs > 300_000)) {
137
+ if (value.waitReadyMs !== undefined && (typeof value.waitReadyMs !== "number" || !Number.isSafeInteger(value.waitReadyMs) || value.waitReadyMs < 0 || value.waitReadyMs > 300_000)) {
96
138
  throw new Error("invalid session reset waitReadyMs");
97
139
  }
98
140
  if (Object.keys(value).some((key) => !["agentId", "authorization", "operation", "waitReadyMs"].includes(key))) {
@@ -100,6 +142,17 @@ function parseRequest(line) {
100
142
  }
101
143
  return value;
102
144
  }
145
+ if (value.operation === "session-recover") {
146
+ if (value.reason !== "context-overflow")
147
+ throw new Error("session recovery requires reason=context-overflow");
148
+ if (value.waitReadyMs !== undefined && (typeof value.waitReadyMs !== "number" || !Number.isSafeInteger(value.waitReadyMs) || value.waitReadyMs < 0 || value.waitReadyMs > 300_000)) {
149
+ throw new Error("invalid session recovery waitReadyMs");
150
+ }
151
+ if (Object.keys(value).some((key) => !["agentId", "authorization", "operation", "reason", "waitReadyMs"].includes(key))) {
152
+ throw new Error("session recovery control request 包含未知字段");
153
+ }
154
+ return value;
155
+ }
103
156
  if (value.operation !== undefined || !OPERATION_ID.test(String(value.operationId || ""))) {
104
157
  throw new Error("invalid agent upsert request");
105
158
  }
@@ -498,13 +551,14 @@ export function createSupervisorControlServer({ larkinHome, authorityToken, ensu
498
551
  },
499
552
  };
500
553
  }
501
- export function createAgentControlServer({ larkinHome, authorityToken, upsert, resetSession, maxRememberedOperations = 256, }) {
554
+ export function createAgentControlServer({ larkinHome, authorityToken, upsert, resetSession, recoverSession, maxRememberedOperations = 256, }) {
502
555
  let socket = "";
503
556
  let socketRoot = "";
504
557
  let socketIdentity = null;
505
558
  const completed = new Map();
506
559
  const inFlight = new Map();
507
560
  const resetInFlight = new Map();
561
+ const recoveryInFlight = new Map();
508
562
  const agentQueues = new Map();
509
563
  const upsertConflict = (operationId, agentId) => ({
510
564
  ok: false, operationId, agentId, code: "operation_conflict", error: "operationId 已绑定其他 Agent 或操作",
@@ -564,6 +618,52 @@ export function createAgentControlServer({ larkinHome, authorityToken, upsert, r
564
618
  return;
565
619
  }
566
620
  if ("operation" in request) {
621
+ if (request.operation === "session-recover") {
622
+ const recoveryRequest = request;
623
+ let operation = recoveryInFlight.get(recoveryRequest.agentId);
624
+ if (!operation) {
625
+ const executeRecovery = async () => {
626
+ try {
627
+ if (!recoverSession)
628
+ throw new Error("session recovery control unavailable");
629
+ const result = await recoverSession({ agentId: recoveryRequest.agentId, reason: recoveryRequest.reason,
630
+ waitReadyMs: recoveryRequest.waitReadyMs ?? 30_000 });
631
+ return sanitizeSessionRecoveryResponse(result, recoveryRequest.agentId);
632
+ }
633
+ catch (error) {
634
+ const code = typeof error.code === "string"
635
+ ? String(error.code)
636
+ : error instanceof RuntimePrerequisiteError && error.readiness.state === "unavailable"
637
+ ? "runtime_unavailable" : "recovery_refused";
638
+ const projection = error;
639
+ return { ok: false, agentId: recoveryRequest.agentId, code,
640
+ error: recoveryErrorText(code), recoveryCommitted: false,
641
+ generationChanged: false, sessionChanged: false, turns: Math.max(0, Number(projection.turns) || 0),
642
+ runtimeReady: projection.runtimeReady === true, channelConnected: projection.channelConnected === true,
643
+ reconnecting: projection.reconnecting === true, pendingCount: Math.max(0, Number(projection.pendingCount) || 0),
644
+ rearmedCount: Math.max(0, Number(projection.rearmedCount) || 0), replayStatus: "not_started",
645
+ remainingPendingCount: Math.max(0, Number(projection.remainingPendingCount) || 0),
646
+ readyForFreshScenario: false, inboundObserved: false,
647
+ ...(error instanceof RuntimePrerequisiteError ? { readiness: sanitizeRecoveryReadiness(error.readiness) } : {}) };
648
+ }
649
+ };
650
+ const prior = agentQueues.get(recoveryRequest.agentId) ?? Promise.resolve();
651
+ const executing = prior.catch(() => { }).then(executeRecovery);
652
+ let queued;
653
+ queued = executing.finally(() => {
654
+ if (agentQueues.get(recoveryRequest.agentId) === queued)
655
+ agentQueues.delete(recoveryRequest.agentId);
656
+ if (recoveryInFlight.get(recoveryRequest.agentId) === queued)
657
+ recoveryInFlight.delete(recoveryRequest.agentId);
658
+ });
659
+ agentQueues.set(recoveryRequest.agentId, queued);
660
+ recoveryInFlight.set(recoveryRequest.agentId, queued);
661
+ operation = queued;
662
+ }
663
+ const response = await operation;
664
+ connection.end(`${JSON.stringify(response)}\n`);
665
+ return;
666
+ }
567
667
  const resetRequest = request;
568
668
  let operation = resetInFlight.get(resetRequest.agentId);
569
669
  if (!operation) {
@@ -776,6 +876,12 @@ export async function requestSessionReset({ larkinHome, agentId, waitReadyMs = 3
776
876
  request: { operation: "session-reset", agentId, waitReadyMs },
777
877
  });
778
878
  }
879
+ export async function requestSessionRecovery({ larkinHome, agentId, reason = "context-overflow", waitReadyMs = 30_000, }) {
880
+ return requestAgentControl({
881
+ larkinHome, timeoutMs: Math.max(1_000, waitReadyMs + 1_000),
882
+ request: { operation: "session-recover", agentId, reason, waitReadyMs },
883
+ });
884
+ }
779
885
  export function cleanupStaleAgentControlSocket(larkinHome, expectedToken) {
780
886
  const authority = secureAuthority(larkinHome);
781
887
  if (!sameSecret(authority.token, expectedToken))
@@ -104,6 +104,10 @@ export async function main(env = process.env, overrides = {}) {
104
104
  const result = await hostShell.resetSession(request.agentId, request.waitReadyMs);
105
105
  return { ok: result.readyForFreshScenario, agentId: request.agentId, ...result };
106
106
  },
107
+ async recoverSession(request) {
108
+ const result = await hostShell.recoverSession(request.agentId, request.reason, request.waitReadyMs);
109
+ return { ok: result.recoveryCommitted, agentId: request.agentId, ...result };
110
+ },
107
111
  });
108
112
  await controlServer.start();
109
113
  await markConfigAppliedAfterRuntimeReady(env, hostShell.agents, hostShell.start());