@nettee/beacon 0.3.0 → 0.3.1
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 +21 -10
- package/dist/feishu/gateway.js +19 -6
- package/dist/run/orchestrator.js +34 -5
- package/dist/state/trigger-store.js +23 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -371,16 +371,27 @@ as Markdown on stdout.
|
|
|
371
371
|
|
|
372
372
|
Per-Profile state lives below `profiles/<profile-id>/state/`. Trigger claims, normalized inputs, Run state, Final Outcomes, Delivery state, and Schedule cursors use durable JSON snapshots. Records do not expire, and Beacon has no automatic cleanup task. Manually deleting state also deletes its deduplication memory.
|
|
373
373
|
|
|
374
|
-
Every new business Run also owns a permanent Pi session. Its Run
|
|
375
|
-
both `sessionId` and `sessionPath
|
|
376
|
-
`runId`, and the path is the dedicated
|
|
377
|
-
`<pi.session_directory>/<profile-id>/<runId>/`. Pi receives that
|
|
378
|
-
`--session-dir`, `--session-id`, and a readable
|
|
379
|
-
name. The directory contains the Pi JSONL
|
|
380
|
-
by Beacon. A queued Run keeps the same
|
|
381
|
-
written by Beacon 0.1.2 or earlier are
|
|
382
|
-
when recovered. Historical completed
|
|
383
|
-
two session fields because those Runs
|
|
374
|
+
Every new business Run also owns a permanent Pi session by default. Its Run
|
|
375
|
+
record stores both `sessionId` and `sessionPath`. For a fresh Trigger the
|
|
376
|
+
session ID is exactly the Beacon `runId`, and the path is the dedicated
|
|
377
|
+
directory `<pi.session_directory>/<profile-id>/<runId>/`. Pi receives that
|
|
378
|
+
mapping via `--session-dir`, `--session-id`, and a readable
|
|
379
|
+
`Beacon <profile-id> <runId>` name. The directory contains the Pi JSONL
|
|
380
|
+
session file and is never cleaned up by Beacon. A queued Run keeps the same
|
|
381
|
+
mapping after restart. Queued records written by Beacon 0.1.2 or earlier are
|
|
382
|
+
assigned the same deterministic mapping when recovered. Historical completed
|
|
383
|
+
records remain readable and may omit the two session fields because those Runs
|
|
384
|
+
were originally ephemeral.
|
|
385
|
+
|
|
386
|
+
When an inbound Feishu message is a quote-reply (`parent_id`) to a message
|
|
387
|
+
Beacon previously delivered (`reply_text` / `reply_card` / `notify_card`, or a
|
|
388
|
+
failure reply), Beacon looks up that outbound Feishu `message_id` on the
|
|
389
|
+
matching Delivery / notify Delivery (`providerRequestId`), then starts a new
|
|
390
|
+
Run that **reuses the prior Pi session** and appends one new user prompt.
|
|
391
|
+
Follow-up Runs keep their own `runId` but set `sessionId` / `sessionPath` to
|
|
392
|
+
the original session. If the quoted message is not a Beacon Delivery, or the
|
|
393
|
+
Delivery did not persist a Feishu `message_id`, Beacon falls back to a fresh
|
|
394
|
+
session.
|
|
384
395
|
|
|
385
396
|
To locate a session from a reported `run_id`, first find its durable Run
|
|
386
397
|
record, then inspect or export the sole JSONL file in `sessionPath`:
|
package/dist/feishu/gateway.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
2
|
import { buildFeishuFinalOutcomeCard } from "./final-outcome-card.js";
|
|
3
|
+
/** Prefer Feishu's outbound `message_id` so quote-replies can resume that Run's Pi session. */
|
|
4
|
+
export function providerRequestIdFromMessageSend(result) {
|
|
5
|
+
const messageId = result.data?.message_id?.trim();
|
|
6
|
+
return messageId ? { providerRequestId: messageId } : {};
|
|
7
|
+
}
|
|
3
8
|
export function waitForShutdown(signals = process) {
|
|
4
9
|
return new Promise((resolve) => {
|
|
5
10
|
const shutdown = () => {
|
|
@@ -121,28 +126,36 @@ export class FeishuGateway {
|
|
|
121
126
|
async deliver(target, outcome) {
|
|
122
127
|
const message = encodeFeishuFinalOutcome(outcome);
|
|
123
128
|
if (target.kind === "reply") {
|
|
124
|
-
const result = await this.client.im.v1.message.reply({
|
|
129
|
+
const result = (await this.client.im.v1.message.reply({
|
|
125
130
|
path: { message_id: target.messageId },
|
|
126
131
|
data: {
|
|
127
132
|
msg_type: message.msgType,
|
|
128
133
|
content: message.content,
|
|
129
134
|
reply_in_thread: false,
|
|
130
135
|
},
|
|
131
|
-
});
|
|
136
|
+
}));
|
|
132
137
|
assertSucceeded("deliver quoted reply", result);
|
|
133
|
-
|
|
138
|
+
const provider = providerRequestIdFromMessageSend(result);
|
|
139
|
+
if (!provider.providerRequestId) {
|
|
140
|
+
console.error(`[beacon] Feishu quoted reply succeeded without message_id; quote-reply session resume will be unavailable for this Delivery`);
|
|
141
|
+
}
|
|
142
|
+
return provider;
|
|
134
143
|
}
|
|
135
144
|
if (target.kind === "chat") {
|
|
136
|
-
const result = await this.client.im.v1.message.create({
|
|
145
|
+
const result = (await this.client.im.v1.message.create({
|
|
137
146
|
params: { receive_id_type: "chat_id" },
|
|
138
147
|
data: {
|
|
139
148
|
receive_id: target.chatId,
|
|
140
149
|
msg_type: message.msgType,
|
|
141
150
|
content: message.content,
|
|
142
151
|
},
|
|
143
|
-
});
|
|
152
|
+
}));
|
|
144
153
|
assertSucceeded("deliver chat message", result);
|
|
145
|
-
|
|
154
|
+
const provider = providerRequestIdFromMessageSend(result);
|
|
155
|
+
if (!provider.providerRequestId) {
|
|
156
|
+
console.error(`[beacon] Feishu chat message succeeded without message_id; quote-reply session resume will be unavailable for this Delivery`);
|
|
157
|
+
}
|
|
158
|
+
return provider;
|
|
146
159
|
}
|
|
147
160
|
throw new Error("Feishu Gateway cannot deliver to local stdout");
|
|
148
161
|
}
|
package/dist/run/orchestrator.js
CHANGED
|
@@ -121,6 +121,26 @@ function normalizeAgentOutcome(input, outcome, notifyTarget) {
|
|
|
121
121
|
}
|
|
122
122
|
return outcome;
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Walk a Feishu quote chain (oldest → newest) from the direct parent backward
|
|
126
|
+
* and reuse the first Beacon Delivery's Pi session.
|
|
127
|
+
*/
|
|
128
|
+
export async function resolveContinuedPiSession(quotedMessages, findByDeliveredMessageId) {
|
|
129
|
+
for (let index = quotedMessages.length - 1; index >= 0; index -= 1) {
|
|
130
|
+
const quoted = quotedMessages[index];
|
|
131
|
+
const prior = await findByDeliveredMessageId(quoted.messageId);
|
|
132
|
+
if (!prior?.run?.sessionId || !prior.run.sessionPath)
|
|
133
|
+
continue;
|
|
134
|
+
return {
|
|
135
|
+
sessionId: prior.run.sessionId,
|
|
136
|
+
sessionPath: prior.run.sessionPath,
|
|
137
|
+
...(prior.run.systemPrompt
|
|
138
|
+
? { systemPrompt: prior.run.systemPrompt }
|
|
139
|
+
: {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
124
144
|
export class RunOrchestrator {
|
|
125
145
|
options;
|
|
126
146
|
now;
|
|
@@ -150,12 +170,15 @@ export class RunOrchestrator {
|
|
|
150
170
|
triggerFor(record, input) {
|
|
151
171
|
return agentSystemPromptTrigger(input ?? record.input, record.notifyTarget);
|
|
152
172
|
}
|
|
153
|
-
newRun(runId, state, trigger, failure) {
|
|
173
|
+
newRun(runId, state, trigger, failure, continued) {
|
|
154
174
|
const timestamp = this.timestamp();
|
|
175
|
+
const sessionId = continued?.sessionId ?? runId;
|
|
176
|
+
const sessionPath = continued?.sessionPath ??
|
|
177
|
+
join(this.options.sessionDirectory, this.options.profile.id, runId);
|
|
155
178
|
return {
|
|
156
179
|
runId,
|
|
157
|
-
sessionId
|
|
158
|
-
sessionPath
|
|
180
|
+
sessionId,
|
|
181
|
+
sessionPath,
|
|
159
182
|
state,
|
|
160
183
|
queuedAt: timestamp,
|
|
161
184
|
...(state === "failed" ? { finishedAt: timestamp } : {}),
|
|
@@ -163,7 +186,7 @@ export class RunOrchestrator {
|
|
|
163
186
|
model: this.options.profile.model.id,
|
|
164
187
|
workspace: this.options.profile.workspace,
|
|
165
188
|
promptDigest: profilePromptDigest(this.options.profile),
|
|
166
|
-
systemPrompt: this.agentSystemPrompt(trigger),
|
|
189
|
+
systemPrompt: continued?.systemPrompt ?? this.agentSystemPrompt(trigger),
|
|
167
190
|
...(failure ? { failure } : {}),
|
|
168
191
|
};
|
|
169
192
|
}
|
|
@@ -363,10 +386,16 @@ export class RunOrchestrator {
|
|
|
363
386
|
await this.deliverOutcome(triggerKey);
|
|
364
387
|
return;
|
|
365
388
|
}
|
|
389
|
+
const continued = input.kind === "feishu_message"
|
|
390
|
+
? await resolveContinuedPiSession(input.quotedMessages, (messageId) => this.options.store.findByDeliveredMessageId(messageId))
|
|
391
|
+
: undefined;
|
|
392
|
+
if (continued) {
|
|
393
|
+
console.log(`[beacon] resuming Pi session session_id=${continued.sessionId} for trigger_key=${triggerKey} run_id=${runId}`);
|
|
394
|
+
}
|
|
366
395
|
await this.options.store.update(triggerKey, (current) => ({
|
|
367
396
|
...current,
|
|
368
397
|
input,
|
|
369
|
-
run: this.newRun(runId, "queued", this.triggerFor(current, input)),
|
|
398
|
+
run: this.newRun(runId, "queued", this.triggerFor(current, input), undefined, continued),
|
|
370
399
|
}));
|
|
371
400
|
if (input.kind === "event") {
|
|
372
401
|
await this.options.queue.enqueueWhenAvailable(() => this.execute(triggerKey, input, runId));
|
|
@@ -139,10 +139,9 @@ function validateRecord(value) {
|
|
|
139
139
|
throw new Error("Run record must contain both Pi sessionId and sessionPath or neither");
|
|
140
140
|
}
|
|
141
141
|
if (record.run?.sessionId &&
|
|
142
|
-
(record.run.
|
|
143
|
-
basename(record.run.sessionPath) !== record.run.runId ||
|
|
142
|
+
(basename(record.run.sessionPath) !== record.run.sessionId ||
|
|
144
143
|
basename(dirname(record.run.sessionPath)) !== record.profileId)) {
|
|
145
|
-
throw new Error("Run Pi session
|
|
144
|
+
throw new Error("Run Pi session path must map to its Profile ID and session ID");
|
|
146
145
|
}
|
|
147
146
|
if (record.delivery && !record.finalOutcome) {
|
|
148
147
|
throw new Error("Trigger record with Delivery must contain a Final Outcome");
|
|
@@ -295,4 +294,25 @@ export class TriggerStore {
|
|
|
295
294
|
return records.sort((left, right) => left.acceptedAt.localeCompare(right.acceptedAt) ||
|
|
296
295
|
left.triggerId.localeCompare(right.triggerId));
|
|
297
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Locate the Trigger whose reply or notify Delivery produced this Feishu
|
|
299
|
+
* outbound `message_id`. Newest accepted Trigger wins on collision.
|
|
300
|
+
*/
|
|
301
|
+
async findByDeliveredMessageId(messageId) {
|
|
302
|
+
const trimmed = messageId.trim();
|
|
303
|
+
if (!trimmed)
|
|
304
|
+
return undefined;
|
|
305
|
+
const records = await this.list();
|
|
306
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
307
|
+
const record = records[index];
|
|
308
|
+
for (const field of ["delivery", "notifyDelivery"]) {
|
|
309
|
+
const delivery = record[field];
|
|
310
|
+
if (delivery?.state === "delivered" &&
|
|
311
|
+
delivery.providerRequestId === trimmed) {
|
|
312
|
+
return record;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
298
318
|
}
|