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.
- package/README.md +72 -33
- package/assets/readme/larkin-hero.svg +111 -0
- package/dist/agent/agent-state-store.cjs +321 -22
- package/dist/agent/agent-state-store.mjs +320 -22
- package/dist/agent/agent-transport.cjs +6 -4
- package/dist/agent/agent-transport.mjs +6 -4
- package/dist/agent/host-reminder-orchestrator.cjs +9 -3
- package/dist/agent/host-reminder-orchestrator.mjs +9 -3
- package/dist/agent/inbox-projection.cjs +114 -23
- package/dist/agent/inbox-projection.mjs +112 -23
- package/dist/app/cli.mjs +3 -1
- package/dist/app/local-control.mjs +108 -2
- package/dist/app/runtime-process.mjs +4 -0
- package/dist/app/session-cli.mjs +55 -20
- package/dist/feishu/document-comment.cjs +2 -1
- package/dist/feishu/document-comment.mjs +2 -1
- package/dist/feishu/host-business-state.cjs +7 -2
- package/dist/feishu/host-business-state.mjs +7 -2
- package/dist/feishu/host-shell.cjs +138 -32
- package/dist/feishu/host-shell.mjs +138 -32
- package/dist/runtime/pi-compaction-recovery.cjs +623 -0
- package/dist/runtime/pi-compaction-recovery.mjs +602 -0
- package/dist/runtime/pi-provider-config.cjs +2 -2
- package/dist/runtime/pi-provider-config.mjs +2 -2
- package/dist/runtime/pi-rpc-client.cjs +39 -22
- package/dist/runtime/pi-rpc-client.mjs +39 -22
- package/dist/runtime/provider-error-classifier.cjs +39 -0
- package/dist/runtime/provider-error-classifier.mjs +34 -0
- package/dist/runtime/runtime-adapters.mjs +82 -19
- package/dist/runtime/runtime-host.cjs +663 -12
- package/dist/runtime/runtime-host.mjs +661 -11
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
131
|
+
/** Return an allowlisted canonical target when the envelope is locatable. */
|
|
36
132
|
function targetOfInboxEnvelope(envelope) {
|
|
37
|
-
|
|
38
|
-
return
|
|
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
|
-
|
|
43
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
122
|
+
/** Return an allowlisted canonical target when the envelope is locatable. */
|
|
29
123
|
export function targetOfInboxEnvelope(envelope) {
|
|
30
|
-
|
|
31
|
-
return
|
|
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
|
-
|
|
36
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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());
|