@fastagent-sh/fastagent 0.18.0 → 0.20.0
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 +25 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +23 -0
- package/dist/atomic-write.js +53 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +55 -0
- package/dist/channels/agentcore-service.js +107 -0
- package/dist/channels/agentcore.d.ts +22 -20
- package/dist/channels/agentcore.js +20 -11
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +55 -7
- package/dist/channels/control.js +312 -67
- package/dist/channels/discover.d.ts +59 -0
- package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.js +6 -9
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +41 -24
- package/dist/channels/feishu/invoke-turn.d.ts +1 -1
- package/dist/channels/feishu/invoke-turn.js +3 -4
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +1 -109
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/{state.js → kit/state.js} +4 -7
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
- package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
- package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
- package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +2 -2
- package/dist/channels/slack/invoke-turn.js +13 -6
- package/dist/channels/slack/onboard.js +5 -5
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +23 -55
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +20 -20
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +67 -13
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +3 -3
- package/dist/channels/telegram/telegram.js +60 -72
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/add-slack.js +6 -5
- package/dist/cli/commands/add.js +1 -5
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/deploy.d.ts +50 -5
- package/dist/cli/commands/deploy.js +183 -134
- package/dist/cli/commands/dev.js +25 -40
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +7 -5
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +80 -121
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +22 -70
- package/dist/cli/serve.js +66 -257
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +53 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +12 -10
- package/dist/deploy/agentcore/plan.js +39 -31
- package/dist/deploy/agentcore/run.d.ts +5 -4
- package/dist/deploy/agentcore/run.js +13 -36
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +18 -1
- package/dist/deploy/docker/run.js +10 -0
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +7 -22
- package/dist/deploy/fly/run.d.ts +9 -23
- package/dist/deploy/fly/run.js +32 -68
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +4 -5
- package/dist/deploy/railway/plan.js +11 -26
- package/dist/deploy/railway/run.d.ts +9 -9
- package/dist/deploy/railway/run.js +40 -40
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +64 -12
- package/dist/dev-supervisor.js +11 -6
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +295 -0
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +5 -5
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +67 -42
- package/dist/engines/pi/create.js +142 -72
- package/dist/engines/pi/definition.d.ts +42 -8
- package/dist/engines/pi/definition.js +114 -11
- package/dist/engines/pi/invoke-session.d.ts +22 -19
- package/dist/engines/pi/invoke-session.js +286 -100
- package/dist/engines/pi/login.js +39 -20
- package/dist/engines/pi/models.d.ts +14 -0
- package/dist/engines/pi/models.js +9 -1
- package/dist/engines/pi/open.d.ts +10 -8
- package/dist/engines/pi/open.js +21 -11
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +27 -0
- package/dist/engines/pi/session-builder.js +87 -118
- package/dist/engines/pi/session-control.d.ts +28 -14
- package/dist/engines/pi/session-control.js +570 -438
- package/dist/engines/pi/session-inheritance.d.ts +30 -0
- package/dist/engines/pi/session-inheritance.js +261 -0
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +15 -3
- package/dist/engines/pi/session-settings.js +22 -1
- package/dist/engines/pi/session-store.d.ts +140 -0
- package/dist/engines/pi/session-store.js +666 -0
- package/dist/engines/pi/tool-context.d.ts +36 -13
- package/dist/engines/pi/tool-context.js +51 -4
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +3 -3
- package/dist/engines/pi/turn-kit.d.ts +32 -6
- package/dist/engines/pi/turn-kit.js +28 -1
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/env.js +1 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +26 -0
- package/dist/paths.js +37 -2
- package/dist/pi.d.ts +14 -8
- package/dist/pi.js +6 -5
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +2 -2
- package/dist/schedule/wakeups.js +11 -8
- package/dist/service.d.ts +157 -0
- package/dist/service.js +376 -0
- package/dist/session-remote.d.ts +8 -3
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +229 -93
- package/dist/session.js +61 -23
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +18 -3
- package/dist/tunnel.js +32 -40
- package/package.json +14 -5
- package/dist/engines/pi/channel.d.ts +0 -37
- package/dist/engines/pi/harness.d.ts +0 -115
- package/dist/engines/pi/harness.js +0 -154
- package/dist/engines/pi/invoke.d.ts +0 -60
- package/dist/engines/pi/invoke.js +0 -459
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -60
- package/dist/engines/pi/sessions.js +0 -464
- package/dist/host/node.js +0 -52
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
|
@@ -29,25 +29,17 @@ import { readBodyCapped } from "../body.js";
|
|
|
29
29
|
import { text } from "../respond.js";
|
|
30
30
|
import { invokeTurn } from "./invoke-turn.js";
|
|
31
31
|
import { collectAttachments, createContextBuffer } from "./context-buffer.js";
|
|
32
|
-
import { attachmentSummary, defaultTelegramRoute, extractFiles, extractImages, fromLabel, messageText, ownFiles, ownImages, pickMessage, telegramEnvelope, } from "./parse.js";
|
|
32
|
+
import { attachmentSummary, defaultTelegramRoute, extractFiles, extractImages, fromLabel, messageText, ownFiles, ownImages, pickMessage, telegramEnvelope, telegramStop, } from "./parse.js";
|
|
33
33
|
import { defaultErrorMessage, streamReply } from "./preview.js";
|
|
34
|
-
import { ensureStateHome } from "../state.js";
|
|
35
|
-
import { dispatchStop } from "../stop-command.js";
|
|
34
|
+
import { ensureStateHome } from "../kit/state.js";
|
|
35
|
+
import { dispatchStop } from "../kit/stop-command.js";
|
|
36
36
|
import { callApi, editMessageText, sendMessage } from "./telegram-api.js";
|
|
37
|
-
import { createTurnQueue } from "../turn-queue.js";
|
|
37
|
+
import { createTurnQueue } from "../kit/turn-queue.js";
|
|
38
|
+
import { commitAnsweredTurn } from "../kit/turn-store.js";
|
|
39
|
+
import { discussionBlock } from "../kit/context-buffer.js";
|
|
38
40
|
import { createTurnStore } from "./turn-store.js";
|
|
39
41
|
// Re-export the public surface authored elsewhere, so `@fastagent-sh/fastagent/telegram` keeps one entry point.
|
|
40
|
-
export { defaultTelegramRoute, telegramEnvelope };
|
|
41
|
-
/** Execution ceiling: a turn that has STARTED running this many times without finishing is dropped
|
|
42
|
-
* rather than run again (a poison turn must not loop forever under a restart policy). Counted per turn
|
|
43
|
-
* at dequeue, so a never-run turn queued behind a poison one keeps its full budget.
|
|
44
|
-
*
|
|
45
|
-
* Known limitation: `startAttempt` cannot tell a self-inflicted process crash from an external SIGTERM
|
|
46
|
-
* (no graceful drain), so a legitimately LONG turn interrupted by this many successive deploys is
|
|
47
|
-
* dropped ("please ask again") as if it were poison. Accepted: catching SIGTERM to spare it would
|
|
48
|
-
* reintroduce the drain the design refuses, and a turn outliving this many deploy cycles is an outlier
|
|
49
|
-
* — raise this constant if such turns are expected. */
|
|
50
|
-
const MAX_TURN_ATTEMPTS = 3;
|
|
42
|
+
export { defaultTelegramRoute, telegramEnvelope, telegramStop };
|
|
51
43
|
/** Update body cap — Telegram updates are small JSON; 1 MiB is generous and guards a public endpoint. */
|
|
52
44
|
const MAX_UPDATE_BYTES = 1 << 20;
|
|
53
45
|
/** Constant-time compare so the secret-token check leaks no timing signal. */
|
|
@@ -77,9 +69,10 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
77
69
|
throw new Error("telegramChannel requires a non-empty botToken (used to send the agent's reply)");
|
|
78
70
|
}
|
|
79
71
|
const formatError = onError ?? defaultErrorMessage;
|
|
80
|
-
// One getMe at startup: the bot's @username (for the default route's group @mention summon
|
|
81
|
-
// not supplied) and the group-privacy flag — privacy
|
|
82
|
-
// group messages that feed the context buffer, so
|
|
72
|
+
// One getMe at startup: the bot's @username (for the default route's group @mention summon and for
|
|
73
|
+
// recognising an addressed `/stop`, only when not supplied) and the group-privacy flag — privacy
|
|
74
|
+
// mode off is required to receive the un-summoned group messages that feed the context buffer, so
|
|
75
|
+
// warn if it is on.
|
|
83
76
|
let mentionName = botUsername;
|
|
84
77
|
void callApi(apiBaseUrl, botToken, "getMe", {}).then((me) => {
|
|
85
78
|
if (mentionName === undefined)
|
|
@@ -88,7 +81,11 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
88
81
|
log.warn("[telegram] privacy mode is on: the bot only sees @mentions / replies / commands, so group " +
|
|
89
82
|
"context (un-summoned messages) won't be captured. Disable it via @BotFather → /setprivacy.");
|
|
90
83
|
}
|
|
91
|
-
},
|
|
84
|
+
},
|
|
85
|
+
// Name every capability the missing username costs, so "the bot ignores /stop@name" is
|
|
86
|
+
// diagnosable from this ONE line — the alternative, a warn per undecidable message, repeats a
|
|
87
|
+
// single startup fact on every update.
|
|
88
|
+
(e) => log.warn(`[telegram] getMe failed; @mention summon, addressed /stop, and the privacy check are skipped: ${String(e)}`));
|
|
92
89
|
// A bot token is "<bot_id>:<secret>" — the bot's own id is knowable synchronously, so reply-to-bot
|
|
93
90
|
// targeting is precise from the first update (no getMe race; getMe only resolves the @username).
|
|
94
91
|
// Every real token parses; one that doesn't (a mock/test token) degrades visibly: reply summon stays
|
|
@@ -154,10 +151,10 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
154
151
|
await notices.get(rec.id);
|
|
155
152
|
notices.delete(rec.id);
|
|
156
153
|
// Count this execution against the durable record (poison-turn ceiling) before running it again.
|
|
157
|
-
const decision = store.startAttempt(rec.id
|
|
154
|
+
const decision = store.startAttempt(rec.id);
|
|
158
155
|
if (decision === "exceeded") {
|
|
159
|
-
// Started
|
|
160
|
-
// any), drop it.
|
|
156
|
+
// Started the ceiling's worth of times without finishing (turn-store.ts MAX_TURN_ATTEMPTS) —
|
|
157
|
+
// tell the asker (reusing its ⏳ notice if any), drop it.
|
|
161
158
|
notifyDropped(rec);
|
|
162
159
|
return;
|
|
163
160
|
}
|
|
@@ -182,7 +179,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
182
179
|
// Fold the un-summoned discussion since the last answered turn into the prompt; it is cleared
|
|
183
180
|
// only when the turn COMPLETES (then it lives in the session).
|
|
184
181
|
const { text: recent, consumed } = buffer.peek(rec.placeKey);
|
|
185
|
-
const prompt =
|
|
182
|
+
const prompt = `${discussionBlock(recent)}${rec.baseText}`;
|
|
186
183
|
const buffered = collectAttachments(consumed, {
|
|
187
184
|
files: new Set(rec.fileIds),
|
|
188
185
|
images: new Set(rec.imageFileIds),
|
|
@@ -204,14 +201,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
204
201
|
fileIds: rec.fileIds,
|
|
205
202
|
},
|
|
206
203
|
buffered,
|
|
207
|
-
}, () => {
|
|
208
|
-
// On completed, ORDER the two durable clears so a crash between them can't replay a
|
|
209
|
-
// context-stripped turn: drop the intent FIRST (a crash after this won't replay it), THEN
|
|
210
|
-
// commit the context buffer (a crash before this just re-folds the same context into the
|
|
211
|
-
// next summon — harmless and additive). The reverse order would leave intent+no-context.
|
|
212
|
-
store.remove(rec.id);
|
|
213
|
-
buffer.commit(rec.placeKey, consumed);
|
|
214
|
-
}), apiBaseUrl, botToken, target, formatError, rec.previewId);
|
|
204
|
+
}, () => commitAnsweredTurn(store, buffer, { id: rec.id, bufferKey: rec.placeKey, consumed })), apiBaseUrl, botToken, target, formatError, rec.previewId);
|
|
215
205
|
log.info(`[telegram] turn done: turn=${rec.id} session=${rec.session} (${Date.now() - startedAt}ms)`);
|
|
216
206
|
}
|
|
217
207
|
catch (error) {
|
|
@@ -313,47 +303,45 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
313
303
|
}
|
|
314
304
|
return new Response(null, { status: 200 });
|
|
315
305
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}, true);
|
|
356
|
-
}
|
|
306
|
+
const session = r.session ?? placeKey;
|
|
307
|
+
const chatId = r.chatId ?? m.chat.id;
|
|
308
|
+
// Reply to the summoning message in groups (threads the answer under the asker); a 1:1 DM needs no
|
|
309
|
+
// reply-quote. Only when the RESOLVED target is the message's own chat+thread: a route that
|
|
310
|
+
// redirects elsewhere must not carry a reply_parameters that resolves in the wrong place (fail, or
|
|
311
|
+
// quote a same-id message there). Compare VALUES, not whether the route touched the field — a route
|
|
312
|
+
// that explicitly returns the same chat/thread still quotes.
|
|
313
|
+
const threadId = r.threadId ?? m.message_thread_id;
|
|
314
|
+
const sameTarget = String(chatId) === String(m.chat.id) && threadId === m.message_thread_id;
|
|
315
|
+
const replyTo = m.chat.type !== "private" && sameTarget ? m.message_id : undefined;
|
|
316
|
+
// Explicit user stop (`/stop`): a control action, never a turn — it must not queue behind the
|
|
317
|
+
// run it stops. Awaited before the ACK: dispatch + one sendMessage is fast, and a delivery failure
|
|
318
|
+
// logs instead of failing the webhook. Read AFTER the route, on the route's own session: the route
|
|
319
|
+
// is both the gate (a route that ignores this chat must not have the bot abort or answer in it) and
|
|
320
|
+
// the session authority (a route that remaps `session` would otherwise have its stop abort a
|
|
321
|
+
// session nobody runs). The default route summons an ADDRESSED `/stop` in a group for this reason:
|
|
322
|
+
// the general slash-command refusal would leave it buffered as discussion while the run kept going.
|
|
323
|
+
// An unaddressed one is not a command at all (see telegramStop) and never reaches here.
|
|
324
|
+
if (telegramStop(update, { botUsername: mentionName, botId })) {
|
|
325
|
+
const feedback = await dispatchStop(control, session, "[telegram]");
|
|
326
|
+
await sendMessage(apiBaseUrl, botToken, { chatId, threadId, replyTo }, feedback, { html: false }).catch((e) => log.warn(`[telegram] stop feedback failed: ${String(e)}`));
|
|
327
|
+
return new Response(null, { status: 200 });
|
|
328
|
+
}
|
|
329
|
+
const baseText = r.text ?? telegramEnvelope(m);
|
|
330
|
+
const imageFileIds = extractImages(m);
|
|
331
|
+
const fileIds = extractFiles(m);
|
|
332
|
+
if (baseText.trim() !== "" || imageFileIds.length > 0 || fileIds.length > 0) {
|
|
333
|
+
// Everything the turn needs, as a plain record; persisted pre-ACK then run serially per session.
|
|
334
|
+
submit({
|
|
335
|
+
id: `${update.update_id}`,
|
|
336
|
+
session,
|
|
337
|
+
placeKey,
|
|
338
|
+
baseText,
|
|
339
|
+
chatId,
|
|
340
|
+
threadId,
|
|
341
|
+
replyTo,
|
|
342
|
+
imageFileIds,
|
|
343
|
+
fileIds,
|
|
344
|
+
}, true);
|
|
357
345
|
}
|
|
358
346
|
return new Response(null, { status: 200 });
|
|
359
347
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a
|
|
3
3
|
* turn), its IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store
|
|
4
|
-
* (../turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
|
|
4
|
+
* (../kit/turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
|
|
5
5
|
* turn execution ceiling, the fail-closed attempt bump).
|
|
6
6
|
*/
|
|
7
|
-
import { type TurnStore } from "../turn-store.ts";
|
|
7
|
+
import { type TurnStore } from "../kit/turn-store.ts";
|
|
8
8
|
/** An accepted turn's persisted intent — the SOURCE for the fields a runner needs to re-execute it
|
|
9
9
|
* (telegram.ts's PendingTurn derives from this, so a new execution field added here propagates and
|
|
10
10
|
* cannot silently drop from the persisted record). Minus the live `previewId` (a restart's queue
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a
|
|
3
3
|
* turn), its IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store
|
|
4
|
-
* (../turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
|
|
4
|
+
* (../kit/turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
|
|
5
5
|
* turn execution ceiling, the fail-closed attempt bump).
|
|
6
6
|
*/
|
|
7
|
-
import { createTurnStore as createGenericTurnStore } from "../turn-store.js";
|
|
7
|
+
import { createTurnStore as createGenericTurnStore } from "../kit/turn-store.js";
|
|
8
8
|
/** State files are an IO boundary: valid JSON of the WRONG SHAPE must degrade like a corrupt file
|
|
9
9
|
* (warn + empty), not flow in as trusted data (mirrors context-buffer's isBufferEntry). */
|
|
10
10
|
function isStoredTurn(t) {
|
package/dist/cli/add-slack.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
27
27
|
throw new Error("`add slack` needs an interactive terminal for internal-app creation and OAuth — " +
|
|
28
28
|
"re-run in a terminal, or pass --no-onboard to scaffold only");
|
|
29
29
|
}
|
|
30
|
-
let state =
|
|
30
|
+
let state = readSlackOnboardingState(input.stateRoot);
|
|
31
31
|
const resumed = state !== undefined;
|
|
32
32
|
if (input.replaceConfig && !state) {
|
|
33
33
|
throw new Error("--replace-config found no local Slack onboarding state on this machine — nothing to replace. " +
|
|
@@ -84,7 +84,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
84
84
|
if (!configToken.startsWith("xoxe.") || !configRefreshToken.startsWith("xoxe-")) {
|
|
85
85
|
throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
writeSlackOnboardingState(input.stateRoot, {
|
|
88
88
|
...state,
|
|
89
89
|
configToken,
|
|
90
90
|
configRefreshToken,
|
|
@@ -116,11 +116,11 @@ export async function onboardSlackInternalApp(input) {
|
|
|
116
116
|
configToken,
|
|
117
117
|
configRefreshToken,
|
|
118
118
|
});
|
|
119
|
-
|
|
119
|
+
writeSlackOnboardingState(input.stateRoot, state);
|
|
120
120
|
}
|
|
121
121
|
else if (input.groupBehavior.explicit && !state.appId) {
|
|
122
122
|
state = { ...state, groupBehavior: input.groupBehavior.behavior };
|
|
123
|
-
|
|
123
|
+
writeSlackOnboardingState(input.stateRoot, state);
|
|
124
124
|
}
|
|
125
125
|
if (state.createAttemptedAt && !state.appId) {
|
|
126
126
|
throw new Error(`a prior Slack app creation attempt at ${state.createAttemptedAt} returned no app ID — ` +
|
|
@@ -144,6 +144,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
144
144
|
action = answer;
|
|
145
145
|
}
|
|
146
146
|
if (action === "replace-config") {
|
|
147
|
+
console.error(`[fastagent] generate a fresh App Configuration Token pair at ${CONFIG_TOKEN_URL}`);
|
|
147
148
|
openExternalUrl(CONFIG_TOKEN_URL);
|
|
148
149
|
state = {
|
|
149
150
|
...state,
|
|
@@ -154,7 +155,7 @@ export async function onboardSlackInternalApp(input) {
|
|
|
154
155
|
if (!state.configToken.startsWith("xoxe.") || !state.configRefreshToken.startsWith("xoxe-")) {
|
|
155
156
|
throw new Error("invalid Slack configuration token prefix (expected xoxe. access + xoxe- refresh)");
|
|
156
157
|
}
|
|
157
|
-
|
|
158
|
+
writeSlackOnboardingState(input.stateRoot, state);
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
const server = await startSlackSetupServer();
|
package/dist/cli/commands/add.js
CHANGED
|
@@ -8,13 +8,9 @@ import { join, relative, resolve } from "node:path";
|
|
|
8
8
|
import { isCancel, select } from "@clack/prompts";
|
|
9
9
|
import { onboardFeishuCloudApp } from "../add-feishu.js";
|
|
10
10
|
import { dotEnvPath, loadDotEnv } from "../../env.js";
|
|
11
|
-
import { resolveStateRoot } from "../../paths.js";
|
|
12
|
-
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
11
|
+
import { resolveStateRoot, SECRETS_DIRNAME, isUnderDir, displayPath, exists } from "../../paths.js";
|
|
13
12
|
import { detectRuntime, readPackageJson } from "../../runtime.js";
|
|
14
|
-
import { isUnderDir } from "../../engines/pi/definition.js";
|
|
15
|
-
import { displayPath } from "../../paths.js";
|
|
16
13
|
import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, } from "../../scaffold/add-channel.js";
|
|
17
|
-
import { exists } from "../../paths.js";
|
|
18
14
|
import { vendorSkill } from "../../scaffold/vendor-skill.js";
|
|
19
15
|
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
20
16
|
/** `fastagent add <kind> [dir]`: scaffold `channels/<kind>.ts` — the adapter import plus a starter `on()`. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SessionControl, type SessionEntry } from "../../session.ts";
|
|
2
2
|
export interface AttachOptions {
|
|
3
3
|
/** Override the control endpoint (skip control.json discovery) — for a remote serve. */
|
|
4
4
|
url?: string;
|
|
@@ -63,7 +63,7 @@ export interface AttachIo {
|
|
|
63
63
|
warn: (line: string) => void;
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
* The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a
|
|
66
|
+
* The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a leaf move, or a
|
|
67
67
|
* compaction, leaves sibling branches behind), and printing the slice raw renders the abandoned
|
|
68
68
|
* branch interleaved with the live one as if it were one conversation. Reachability is computed
|
|
69
69
|
* backwards from the leaf; an entry whose parent lies BEFORE the slice is path-connected by
|
|
@@ -106,5 +106,3 @@ settleMs?: number): Promise<{
|
|
|
106
106
|
cursor: string | undefined;
|
|
107
107
|
sawProgress: boolean;
|
|
108
108
|
}>;
|
|
109
|
-
/** The cursor a failed round reached, if it recorded one. */
|
|
110
|
-
export declare function roundCursor(error: unknown): string | undefined;
|
|
@@ -16,7 +16,7 @@ import { loadDotEnv } from "../../env.js";
|
|
|
16
16
|
import { resolveStateRoot } from "../../paths.js";
|
|
17
17
|
import { log, setLogLevel } from "../../log.js";
|
|
18
18
|
import { ABORTED_CODE, SESSION_BUSY_CODE } from "../../agent.js";
|
|
19
|
-
import { NO_ACTIVE_RUN_CODE } from "../../session.js";
|
|
19
|
+
import { NO_ACTIVE_RUN_CODE, } from "../../session.js";
|
|
20
20
|
import { ControlRequestError, connectAgent, connectSessionControl } from "../../session-remote.js";
|
|
21
21
|
import { failStartup, placementOrExit } from "../fail.js";
|
|
22
22
|
/** Read the serving process's local discovery file. */
|
|
@@ -157,7 +157,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
157
157
|
if (!endpoint)
|
|
158
158
|
endpoint = discover(dir); // discovered: the first read shares the budget
|
|
159
159
|
const connected = await connectSessionControl(endpoint);
|
|
160
|
-
return { control: connected, state: await connected.
|
|
160
|
+
return { control: connected, state: await connected.sessions.get(sessionArg).state() };
|
|
161
161
|
}
|
|
162
162
|
catch (error) {
|
|
163
163
|
const decision = decideRound(errorFacts(error), {
|
|
@@ -208,7 +208,7 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
208
208
|
// ABORTED_CODE: a deliberate stop from ANY client (this attach's /abort, another attach, a
|
|
209
209
|
// Web panel) — the settled line reports it either way; no source discrimination here.
|
|
210
210
|
}
|
|
211
|
-
// session_busy = the LEASE is held — by another run OR a boundary mutation (compact/
|
|
211
|
+
// session_busy = the LEASE is held — by another run OR a boundary mutation (compact/update
|
|
212
212
|
// contend on the same lease); "a run started" would be a guess, and "steer it" bad advice
|
|
213
213
|
// against a compact. State the lease fact, promise nothing.
|
|
214
214
|
if (sawBusy)
|
|
@@ -225,12 +225,14 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
225
225
|
void answerSlashInput(trimmed, control, (l) => console.log(l));
|
|
226
226
|
return;
|
|
227
227
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
228
|
+
const stopping = trimmed === "/abort";
|
|
229
|
+
const label = stopping ? "abort" : "steer";
|
|
230
|
+
const session = control.sessions.get(sessionArg);
|
|
231
|
+
void (stopping ? session.abort() : session.steer({ text: trimmed })).then((result) => {
|
|
230
232
|
if (result.ok) {
|
|
231
|
-
console.log(`[${
|
|
233
|
+
console.log(`[${label} accepted]`);
|
|
232
234
|
}
|
|
233
|
-
else if (
|
|
235
|
+
else if (!stopping && result.error.code === NO_ACTIVE_RUN_CODE) {
|
|
234
236
|
// The invoke stream attach holds IS this run's driver (design: disconnect = cancel), so
|
|
235
237
|
// unlike channel-started runs, this one dies with the attach. Say so up front.
|
|
236
238
|
// Intent, not fact: admission may still fail (session_busy — the lease could be held
|
|
@@ -240,9 +242,9 @@ export async function runAttach(sessionArg, dirArg, opts) {
|
|
|
240
242
|
void startRun(trimmed).catch((error) => console.log(`[prompt failed: ${String(error)}]`));
|
|
241
243
|
}
|
|
242
244
|
else {
|
|
243
|
-
console.log(`[${
|
|
245
|
+
console.log(`[${label} rejected: ${result.error.code} — ${result.error.message}]`);
|
|
244
246
|
}
|
|
245
|
-
}, (error) => console.log(`[${
|
|
247
|
+
}, (error) => console.log(`[${label} failed: ${String(error)}]`));
|
|
246
248
|
});
|
|
247
249
|
// Every round has ONE shape: subscribe → backfill (render the durable record since the cursor)
|
|
248
250
|
// → drain live until the stream drops. Subscribing first + the server's eager registration
|
|
@@ -424,7 +426,7 @@ const isAuthError = (error) => error instanceof ControlRequestError && error.sta
|
|
|
424
426
|
* startup (a serve restart hours in), so the local name must not imply "startup only". */
|
|
425
427
|
const exitWith = failStartup;
|
|
426
428
|
/**
|
|
427
|
-
* The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a
|
|
429
|
+
* The backfill slice REDUCED TO THE ACTIVE PATH — a session is a tree (a leaf move, or a
|
|
428
430
|
* compaction, leaves sibling branches behind), and printing the slice raw renders the abandoned
|
|
429
431
|
* branch interleaved with the live one as if it were one conversation. Reachability is computed
|
|
430
432
|
* backwards from the leaf; an entry whose parent lies BEFORE the slice is path-connected by
|
|
@@ -437,7 +439,7 @@ export function activePathSlice(entries, leafEntryId) {
|
|
|
437
439
|
return entries;
|
|
438
440
|
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
439
441
|
// An append always moves the leaf, so a leaf BEHIND the slice means every entry in it was
|
|
440
|
-
// appended and then abandoned — a
|
|
442
|
+
// appended and then abandoned — a leaf move backwards with no new turn since. Nothing here is on
|
|
441
443
|
// the active path.
|
|
442
444
|
if (!byId.has(leafEntryId))
|
|
443
445
|
return [];
|
|
@@ -509,7 +511,7 @@ settleMs = 300) {
|
|
|
509
511
|
// The round HOLDS its subscription's iterator: one round = one stream, on every path — a
|
|
510
512
|
// backfill failure must close it before propagating, or the caller's retry round would stack a
|
|
511
513
|
// second concurrent stream interleaving the same session's output.
|
|
512
|
-
const iterator = control.
|
|
514
|
+
const iterator = control.sessions.get(session).events()[Symbol.asyncIterator]();
|
|
513
515
|
// Live output is BUFFERED while the replay block prints, then flushed — the drain runs
|
|
514
516
|
// concurrently with the backfill, and interleaving raw deltas into the labeled replay would make
|
|
515
517
|
// both unreadable; the [end of replay] label is only honest if the block is contiguous.
|
|
@@ -554,7 +556,7 @@ settleMs = 300) {
|
|
|
554
556
|
let next = cursor;
|
|
555
557
|
let sawBackfill = false;
|
|
556
558
|
try {
|
|
557
|
-
const backfill = await control.entries(
|
|
559
|
+
const backfill = await control.sessions.get(session).entries(cursor !== undefined ? { since: cursor } : undefined);
|
|
558
560
|
sawBackfill = backfill.entries.length > 0;
|
|
559
561
|
// Advance by APPEND ORDER (the last returned record), never by leafEntryId: `since` is an
|
|
560
562
|
// append-position cursor (design §7), and a leaf that sits before later appends (abandoned
|
|
@@ -579,7 +581,7 @@ settleMs = 300) {
|
|
|
579
581
|
}
|
|
580
582
|
// The protocol's reconnect step the replay cannot cover: status changes while away are
|
|
581
583
|
// LIVE-only events (state_changed before a restart is neither replayed nor re-emitted).
|
|
582
|
-
const now = await control.
|
|
584
|
+
const now = await control.sessions.get(session).state();
|
|
583
585
|
io.println(`[live — ${now.status}${now.activeRunId ? ` (run ${now.activeRunId})` : ""}]`);
|
|
584
586
|
}
|
|
585
587
|
catch (error) {
|
|
@@ -612,7 +614,7 @@ function withCursor(error, cursor) {
|
|
|
612
614
|
return Object.assign(e, { attachCursor: cursor });
|
|
613
615
|
}
|
|
614
616
|
/** The cursor a failed round reached, if it recorded one. */
|
|
615
|
-
|
|
617
|
+
function roundCursor(error) {
|
|
616
618
|
return error.attachCursor;
|
|
617
619
|
}
|
|
618
620
|
/** Render one durable record on replay — the guaranteed kind vocabulary; other kinds are skipped. */
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/** The deploy targets, as a value: {@link HOST_ONLY_FLAGS} is checked against this list by a test, and
|
|
2
|
+
* a type alone cannot be enumerated at runtime. NOT the only copy — `cli/program.ts` repeats it as the
|
|
3
|
+
* `<host>` argument's `choices`, because that file may not import a command module at load time (its
|
|
4
|
+
* per-command imports are lazy). Adding a host means editing both; the type there rides in on a
|
|
5
|
+
* `import type`, which costs nothing at runtime. */
|
|
6
|
+
export declare const DEPLOY_HOSTS: readonly ["docker", "fly", "railway", "agentcore"];
|
|
7
|
+
export type DeployHost = (typeof DEPLOY_HOSTS)[number];
|
|
2
8
|
export interface DeployOptions {
|
|
3
9
|
run?: boolean;
|
|
4
10
|
tunnel?: boolean;
|
|
@@ -12,12 +18,50 @@ export interface DeployOptions {
|
|
|
12
18
|
/** false ⇔ `--no-input`. */
|
|
13
19
|
input?: boolean;
|
|
14
20
|
}
|
|
21
|
+
/** A flag exactly one host honours, and what the OTHERS do instead. `instead` is exhaustive over the
|
|
22
|
+
* non-owners, so a host added to {@link DEPLOY_HOSTS} cannot silently lose its line. */
|
|
23
|
+
interface HostOnlyFlag<Owner extends DeployHost> {
|
|
24
|
+
flag: string;
|
|
25
|
+
owner: Owner;
|
|
26
|
+
passed: (opts: DeployOptions) => boolean;
|
|
27
|
+
instead: Record<Exclude<DeployHost, Owner>, string>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* ONE table for "this flag belongs to that host", because the fact is symmetric and was not stored
|
|
31
|
+
* that way: each host branch stated the OTHER hosts' flags in its own words, so the same sentence
|
|
32
|
+
* existed three times per flag and had already drifted (`Railway-only` twice, `railway-only` once).
|
|
33
|
+
*
|
|
34
|
+
* Every row is a flag that only WARNS elsewhere. `--tunnel` is host-only too and is deliberately not
|
|
35
|
+
* here: it is a usage GATE (`failUsage`, exit 2) raised in `runDeploy`, not a warning — moving it in
|
|
36
|
+
* would downgrade a refusal to a line of advice.
|
|
37
|
+
*
|
|
38
|
+
* Exported for the exhaustiveness test: the type stops a missing `instead` line, and the test stops it
|
|
39
|
+
* from being typed away.
|
|
40
|
+
*/
|
|
41
|
+
export declare const HOST_ONLY_FLAGS: (HostOnlyFlag<"fly"> | HostOnlyFlag<"railway">)[];
|
|
42
|
+
/** Exported for its own test: nothing covered these six sentences, which is how three of them came to
|
|
43
|
+
* disagree about the spelling of a host name. */
|
|
44
|
+
export declare function warnHostOnlyFlags(host: DeployHost, opts: DeployOptions): void;
|
|
15
45
|
export declare function runDeploy(host: DeployHost, dirArg: string, opts: DeployOptions): Promise<void>;
|
|
16
46
|
/**
|
|
17
|
-
* Write
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
47
|
+
* Write each generated artifact under `target`, under ONE ownership rule: **deploy only ever overwrites
|
|
48
|
+
* its own output.** Each generated file opens with a marker, so a file on disk is either OURS (a previous
|
|
49
|
+
* `deploy` wrote it) or the author's — and `--force` means "my generated artifact is authoritative",
|
|
50
|
+
* which never licenses clobbering a file we did not write. To hand a path back to fastagent, delete it.
|
|
51
|
+
*
|
|
52
|
+
* That single rule replaced a second mechanism (a per-host list of paths exempt from `--force`), which
|
|
53
|
+
* only described one shape: with the agent inside the workspace, the root `.dockerignore` is the
|
|
54
|
+
* WORKSPACE's file and was listed; with the agent flat, the list was empty — so `--force` would have
|
|
55
|
+
* overwritten a hand-written root `Dockerfile` in a repository `init --flat` had explicitly adopted.
|
|
56
|
+
* Ownership is a property of the file, not of where the agent sits.
|
|
57
|
+
*
|
|
58
|
+
* A KEPT file of OURS that no longer matches what deploy would generate now (config/channel/lockfile/
|
|
59
|
+
* version drift — or the author's edits, which we cannot tell apart) is flagged stale; `--force`
|
|
60
|
+
* regenerates that one.
|
|
61
|
+
*
|
|
62
|
+
* Exported for its own test — this is a four-branch state machine over (exists, ours, force) that used
|
|
63
|
+
* to be proven by spawning the CLI eight times, which is command LOGIC re-run through a subprocess
|
|
64
|
+
* (see vitest.config.ts) and the suite's slowest test.
|
|
21
65
|
*/
|
|
22
66
|
export declare function writeArtifacts(target: string, artifacts: {
|
|
23
67
|
path: string;
|
|
@@ -26,3 +70,4 @@ export declare function writeArtifacts(target: string, artifacts: {
|
|
|
26
70
|
force: boolean;
|
|
27
71
|
alwaysWrite?: string[];
|
|
28
72
|
}): Promise<void>;
|
|
73
|
+
export {};
|