@alook/daemon 0.1.16 → 0.1.17
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/dist/cli/index.js +165 -46
- package/dist/index.js +131 -29
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -17792,7 +17792,9 @@ var communityChannel = sqliteTable("community_channel", {
|
|
|
17792
17792
|
creatorId: text("creator_id").references(() => user.id, { onDelete: "set null" }),
|
|
17793
17793
|
messageCount: integer2("message_count").default(0),
|
|
17794
17794
|
archived: integer2("archived").default(0),
|
|
17795
|
-
parentMessageId: text("parent_message_id"),
|
|
17795
|
+
parentMessageId: text("parent_message_id").references(() => communityMessage.id, {
|
|
17796
|
+
onDelete: "cascade"
|
|
17797
|
+
}),
|
|
17796
17798
|
lastMessageAt: text("last_message_at"),
|
|
17797
17799
|
createdAt: text("created_at").notNull().$defaultFn(() => new Date().toISOString())
|
|
17798
17800
|
}, (t) => [
|
|
@@ -18358,8 +18360,9 @@ var communityChannelDeleteSchema = exports_external.strictObject({
|
|
|
18358
18360
|
type: exports_external.literal("community:channel.delete"),
|
|
18359
18361
|
serverId: string4,
|
|
18360
18362
|
channelId: string4,
|
|
18361
|
-
parentChannelId: nullableString.optional()
|
|
18362
|
-
|
|
18363
|
+
parentChannelId: nullableString.optional(),
|
|
18364
|
+
parentMessageId: string4.optional()
|
|
18365
|
+
}).refine((event) => event.parentMessageId === undefined || typeof event.parentChannelId === "string" && event.parentChannelId.length > 0, { message: "parentMessageId requires parentChannelId" });
|
|
18363
18366
|
var positionedIdSchema = exports_external.strictObject({ id: string4, position: exports_external.number() });
|
|
18364
18367
|
var communityChannelReorderSchema = exports_external.strictObject({
|
|
18365
18368
|
type: exports_external.literal("community:channel.reorder"),
|
|
@@ -25980,7 +25983,7 @@ function parseLocalMessageReminderBody(body, agentId) {
|
|
|
25980
25983
|
return null;
|
|
25981
25984
|
if (!Number.isSafeInteger(record4.sentSeq) || record4.sentSeq < 1)
|
|
25982
25985
|
return null;
|
|
25983
|
-
if (!Number.isSafeInteger(record4.remindAfterMs) || record4.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record4.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
|
|
25986
|
+
if (!Number.isSafeInteger(record4.remindAfterMs) || record4.remindAfterMs !== 0 && record4.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record4.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
|
|
25984
25987
|
return null;
|
|
25985
25988
|
return {
|
|
25986
25989
|
agentId,
|
|
@@ -26708,6 +26711,16 @@ function scrubRuntimeErrorDiagnosticText(value) {
|
|
|
26708
26711
|
|
|
26709
26712
|
// src/drivers/systemPrompt.ts
|
|
26710
26713
|
var CLI = "$ALOOK_CLI";
|
|
26714
|
+
var MESSAGE_SEND_STDIN_POLICY = [
|
|
26715
|
+
"`--stdin` is required and limited to 1 KiB of UTF-8. Write it as social language a person " + "with ADHD can scan without effort:",
|
|
26716
|
+
"",
|
|
26717
|
+
"- Lead with the point, result, decision, or one concrete ask.",
|
|
26718
|
+
"- Keep one message to one topic; suppress tangents and repeated recap.",
|
|
26719
|
+
"- Use at most five short items when a list helps.",
|
|
26720
|
+
"- Drop preambles, play-by-play, and closing pleasantries.",
|
|
26721
|
+
"- Put exact plans, reviews, evidence, logs, and long technical detail in a Markdown attachment. " + "The message body carries only the short summary and next action."
|
|
26722
|
+
].join(`
|
|
26723
|
+
`);
|
|
26711
26724
|
function identitySection(config2) {
|
|
26712
26725
|
const parts = ["## Identity", ""];
|
|
26713
26726
|
const name = config2.agentName ?? "a member of the household";
|
|
@@ -26738,9 +26751,9 @@ function cliCommandsSection() {
|
|
|
26738
26751
|
"",
|
|
26739
26752
|
"### Messaging",
|
|
26740
26753
|
"",
|
|
26741
|
-
`1. \`${CLI} inbox pull\` — fetch unread messages
|
|
26742
|
-
`2. \`${CLI} message send\` — send to a channel, DM, or thread. ` + `
|
|
26743
|
-
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted
|
|
26754
|
+
`1. \`${CLI} inbox pull\` — fetch unread messages; \`--no-ack\` peeks without advancing.`,
|
|
26755
|
+
`2. \`${CLI} message send --target <ref> --remind-after <0|Nm|Nh> --stdin\` — send to a ` + `channel, DM, or thread. ` + `The body is required through \`--stdin\` and limited to 1 KiB of UTF-8. ` + `Attach uploaded files with \`--attachment <id>\` (repeatable, order matters). ` + `\`--remind-after\` accepts \`0\`, or a whole-number duration from \`1m\` to \`24h\`.`,
|
|
26756
|
+
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted.`,
|
|
26744
26757
|
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
26745
26758
|
`5. \`${CLI} message emoji --target <ref> --emoji <e>\` — react with a single emoji. ` + `Works on channel messages (\`/<server>/<channel>#N\`), DM messages ` + `(\`/.dm/<peer>#N\`), and thread-reply messages (\`/<server>/<channel>/#N#M\`).`,
|
|
26746
26759
|
`6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
|
|
@@ -26770,7 +26783,7 @@ function cliCommandsSection() {
|
|
|
26770
26783
|
"",
|
|
26771
26784
|
"### Context Lifecycle",
|
|
26772
26785
|
"",
|
|
26773
|
-
`1. \`${CLI} nap --handoff <file>\` — reset
|
|
26786
|
+
`1. \`${CLI} nap --handoff <file>\` — reset the current session from a required handoff file.`,
|
|
26774
26787
|
"",
|
|
26775
26788
|
"### Output format",
|
|
26776
26789
|
"",
|
|
@@ -26786,14 +26799,29 @@ function messagingSection() {
|
|
|
26786
26799
|
"",
|
|
26787
26800
|
"### Sending & receiving",
|
|
26788
26801
|
"",
|
|
26789
|
-
"You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying
|
|
26802
|
+
"You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying; the same sending rules apply either way.",
|
|
26803
|
+
"",
|
|
26804
|
+
"#### Message body",
|
|
26805
|
+
"",
|
|
26806
|
+
MESSAGE_SEND_STDIN_POLICY,
|
|
26807
|
+
"",
|
|
26808
|
+
"#### Follow up when a conversation goes quiet",
|
|
26809
|
+
"",
|
|
26810
|
+
"`--remind-after T` is required on every send and controls whether a local follow-up is " + "armed for the same channel, thread, or DM.",
|
|
26811
|
+
"",
|
|
26812
|
+
"- Use `1m` to `24h` when silence would leave work unfinished — for example, after a question, " + "approval request, handoff, or blocker. If no newer message arrives by T, you will be reminded " + "to return; a newer message or daemon restart cancels the timer.",
|
|
26813
|
+
"- Use `0` only when no later action depends on a reply. It disables the timer.",
|
|
26814
|
+
"",
|
|
26815
|
+
"Example: `--remind-after 5m` asks for a follow-up after five quiet minutes.",
|
|
26816
|
+
"",
|
|
26817
|
+
"#### Sending mechanics",
|
|
26790
26818
|
"",
|
|
26791
|
-
|
|
26792
|
-
|
|
26819
|
+
`- Send body: use \`${CLI} message send --target <ref> --remind-after T --stdin\` with the ` + "quoted-heredoc form under *Message formatting*.",
|
|
26820
|
+
`- Long detail: upload it with \`${CLI} message attachment upload --target <ref> --file <path>.md\`; ` + "add the returned id as `--attachment <id>` on the short stdin send.",
|
|
26821
|
+
`- Cite a specific message: add \`--reply "#37"\` — \`--reply\` takes the \`#N\` seq ` + "(within `--target`) of the message you're answering.",
|
|
26793
26822
|
"",
|
|
26794
|
-
|
|
26795
|
-
|
|
26796
|
-
`- Cite a specific message: add \`--reply "#37"\` to either form — \`--reply\` takes the \`#N\` seq ` + "(within `--target`) of the message you're answering.",
|
|
26823
|
+
"Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, read history (below) or DM the relevant people.",
|
|
26824
|
+
"Write every message body in the stdin/heredoc block; never place it directly on the command line.",
|
|
26797
26825
|
"",
|
|
26798
26826
|
"### Context refs",
|
|
26799
26827
|
"",
|
|
@@ -26827,14 +26855,14 @@ function messagingSection() {
|
|
|
26827
26855
|
"",
|
|
26828
26856
|
"### Message formatting",
|
|
26829
26857
|
"",
|
|
26830
|
-
"Alook
|
|
26858
|
+
"Alook specially renders refs and mentions in message bodies. Write them as plain text, not " + "inside backticks.",
|
|
26831
26859
|
"",
|
|
26832
|
-
"- **Context refs** —
|
|
26860
|
+
"- **Context refs** — write the full refs from *Context refs* above so they stay " + "clickable. Never paste a private DM ref into a server channel.",
|
|
26833
26861
|
"- **Mentions** — `@name#NNNN` calls that person's attention specifically. In a private " + "channel, first verify they " + `are a member with \`${CLI} channel member --channel <ref>\` before mentioning them.`,
|
|
26834
26862
|
"",
|
|
26835
26863
|
"```bash",
|
|
26836
26864
|
"# Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
|
|
26837
|
-
`${CLI} message send --target "/demo#1234/general" --stdin <<'ALOOK_MESSAGE_7F3C'`,
|
|
26865
|
+
`${CLI} message send --target "/demo#1234/general" --remind-after 5m --stdin <<'ALOOK_MESSAGE_7F3C'`,
|
|
26838
26866
|
"@alice#0001 Please review /demo#1234/general#42",
|
|
26839
26867
|
"ALOOK_MESSAGE_7F3C",
|
|
26840
26868
|
"```",
|
|
@@ -26847,7 +26875,7 @@ function messagingSection() {
|
|
|
26847
26875
|
"```",
|
|
26848
26876
|
"",
|
|
26849
26877
|
"`channel` is the reply ref. `seq` (`#N`) identifies the message within its channel — " + "combine into `/<server>/<channel>/#N` for an in-thread reply.",
|
|
26850
|
-
"`content.replyTo` (`{seq, sender}`)
|
|
26878
|
+
"`content.replyTo` (`{seq, sender}`) identifies the message being replied to.",
|
|
26851
26879
|
"`hint` is present when the containing surface changes how you should act. Follow it."
|
|
26852
26880
|
].join(`
|
|
26853
26881
|
`);
|
|
@@ -26860,15 +26888,15 @@ function channelTypesSection() {
|
|
|
26860
26888
|
"",
|
|
26861
26889
|
"### Text channels",
|
|
26862
26890
|
"",
|
|
26863
|
-
"- A text channel is a linear conversation. Send
|
|
26891
|
+
"- A text channel is a linear conversation. Send with `message send --target " + "/<server>/<channel>`.",
|
|
26864
26892
|
"- A text-channel message may have a side thread at `/<server>/<channel>/#N`. Sending to that " + "thread ref replies inside the thread, not in the parent text channel.",
|
|
26865
26893
|
"",
|
|
26866
26894
|
"### Forum channels",
|
|
26867
26895
|
"",
|
|
26868
26896
|
"- A forum is a collection of posts, not one linear conversation.",
|
|
26869
26897
|
"- Each top-level message in a forum is a post title. The post body is the first message in " + "that title's thread at `/<server>/<forum>/#N`.",
|
|
26870
|
-
"- To participate in
|
|
26871
|
-
"- To publish
|
|
26898
|
+
"- To participate in a post, use `message send --target /<server>/<forum>/#N`.",
|
|
26899
|
+
"- To publish a post, use `message send --target /<server>/<forum>` for its title, then " + "`message send --target /<server>/<forum>/#N` for the body."
|
|
26872
26900
|
].join(`
|
|
26873
26901
|
`);
|
|
26874
26902
|
}
|
|
@@ -26896,13 +26924,7 @@ function utilsSection() {
|
|
|
26896
26924
|
"",
|
|
26897
26925
|
"### Join a new server",
|
|
26898
26926
|
"",
|
|
26899
|
-
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
|
|
26900
|
-
"",
|
|
26901
|
-
"### Follow up when a conversation goes quiet",
|
|
26902
|
-
"",
|
|
26903
|
-
`Use \`message send --remind-after <duration>\` when you send something that may need a later ` + "follow-up — for example, a question, approval request, handoff, or blocker — and silence would " + "leave the work unfinished. If no newer message appears in that channel, thread, or DM during " + "the duration, you'll receive a reminder to return and decide what to do next. Don't add it to " + "ordinary messages that need no follow-up.",
|
|
26904
|
-
"",
|
|
26905
|
-
`Example: ${CLI} message send --target "/demo#1234/team" --remind-after 1m --file ./message.md`
|
|
26927
|
+
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
|
|
26906
26928
|
].join(`
|
|
26907
26929
|
`);
|
|
26908
26930
|
}
|
|
@@ -26910,11 +26932,10 @@ function criticalRulesSection() {
|
|
|
26910
26932
|
return [
|
|
26911
26933
|
"## Critical rules",
|
|
26912
26934
|
"",
|
|
26913
|
-
`- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something,
|
|
26935
|
+
`- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, share it through \`${CLI}\`, uploading a file when needed.`,
|
|
26914
26936
|
"- **Never expose tokens, keys, or secrets.** Redact credential-like strings from tool output " + "before sharing.",
|
|
26915
26937
|
"- **Match the sender's language.** Reply in the language they wrote in.",
|
|
26916
|
-
"- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend."
|
|
26917
|
-
"- **Finish in-flight work before stopping.** Don't leave anything half-handled. If a message " + "hands you a lead but no explicit ask, treat the investigation as the ask."
|
|
26938
|
+
"- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend."
|
|
26918
26939
|
].join(`
|
|
26919
26940
|
`);
|
|
26920
26941
|
}
|
|
@@ -26922,7 +26943,7 @@ function executionModelSection() {
|
|
|
26922
26943
|
return [
|
|
26923
26944
|
"## How you work — async, not turn-based",
|
|
26924
26945
|
"",
|
|
26925
|
-
"Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. Stop only when all of it is done.",
|
|
26946
|
+
"Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. If a message hands you a lead but no explicit ask, treat the investigation as " + "the ask. Stop only when all of it is done.",
|
|
26926
26947
|
"",
|
|
26927
26948
|
"On wake, restore durable context from `memory.md` and the context timeline, then pull your inbox. " + "Follow *Outstanding work marks* below before taking new work.",
|
|
26928
26949
|
"",
|
|
@@ -29372,6 +29393,10 @@ class MessageReminderScheduler {
|
|
|
29372
29393
|
}
|
|
29373
29394
|
arm(input) {
|
|
29374
29395
|
const key = reminderKey(input.agentId, input.channel);
|
|
29396
|
+
if (input.remindAfterMs === 0) {
|
|
29397
|
+
this.clearReminder(key);
|
|
29398
|
+
return { armed: false, reason: "disabled" };
|
|
29399
|
+
}
|
|
29375
29400
|
const latest = this.latestObservedSeq.get(key);
|
|
29376
29401
|
if (latest !== undefined && latest > input.sentSeq) {
|
|
29377
29402
|
return { armed: false, reason: "newer_message_observed" };
|
|
@@ -29599,6 +29624,78 @@ function createDaemonAgentDriverHost(ctx, onRawLine) {
|
|
|
29599
29624
|
};
|
|
29600
29625
|
}
|
|
29601
29626
|
|
|
29627
|
+
// src/daemon/daemonSelfSleep.ts
|
|
29628
|
+
var DAEMON_SELF_SLEEP_TIMEOUT_MS = 15 * 24 * 60 * 60 * 1000;
|
|
29629
|
+
var systemClock = {
|
|
29630
|
+
setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
29631
|
+
clearTimer: (timer) => clearTimeout(timer)
|
|
29632
|
+
};
|
|
29633
|
+
|
|
29634
|
+
class DaemonSelfSleepScheduler {
|
|
29635
|
+
opts;
|
|
29636
|
+
clock;
|
|
29637
|
+
workingAgents = new Set;
|
|
29638
|
+
timer = null;
|
|
29639
|
+
generation = 0;
|
|
29640
|
+
started = false;
|
|
29641
|
+
stopped = false;
|
|
29642
|
+
constructor(opts) {
|
|
29643
|
+
this.opts = opts;
|
|
29644
|
+
this.clock = opts.clock ?? systemClock;
|
|
29645
|
+
}
|
|
29646
|
+
start() {
|
|
29647
|
+
if (this.started || this.stopped)
|
|
29648
|
+
return;
|
|
29649
|
+
this.started = true;
|
|
29650
|
+
this.arm();
|
|
29651
|
+
}
|
|
29652
|
+
observeMessage() {
|
|
29653
|
+
if (!this.started || this.stopped)
|
|
29654
|
+
return;
|
|
29655
|
+
this.arm();
|
|
29656
|
+
}
|
|
29657
|
+
observeAgentActivity(agentId, working) {
|
|
29658
|
+
if (this.stopped)
|
|
29659
|
+
return;
|
|
29660
|
+
if (working) {
|
|
29661
|
+
this.workingAgents.add(agentId);
|
|
29662
|
+
if (this.started)
|
|
29663
|
+
this.cancel();
|
|
29664
|
+
return;
|
|
29665
|
+
}
|
|
29666
|
+
if (this.workingAgents.delete(agentId) && this.started && this.workingAgents.size === 0)
|
|
29667
|
+
this.arm();
|
|
29668
|
+
}
|
|
29669
|
+
stop() {
|
|
29670
|
+
if (this.stopped)
|
|
29671
|
+
return;
|
|
29672
|
+
this.stopped = true;
|
|
29673
|
+
this.workingAgents.clear();
|
|
29674
|
+
this.cancel();
|
|
29675
|
+
}
|
|
29676
|
+
arm() {
|
|
29677
|
+
this.cancel();
|
|
29678
|
+
if (this.workingAgents.size > 0)
|
|
29679
|
+
return;
|
|
29680
|
+
const generation = this.generation;
|
|
29681
|
+
this.timer = this.clock.setTimer(() => {
|
|
29682
|
+
if (this.stopped || this.generation !== generation || this.workingAgents.size > 0)
|
|
29683
|
+
return;
|
|
29684
|
+
this.timer = null;
|
|
29685
|
+
this.generation += 1;
|
|
29686
|
+
this.opts.onSleep();
|
|
29687
|
+
}, DAEMON_SELF_SLEEP_TIMEOUT_MS);
|
|
29688
|
+
this.timer.unref?.();
|
|
29689
|
+
}
|
|
29690
|
+
cancel() {
|
|
29691
|
+
this.generation += 1;
|
|
29692
|
+
if (!this.timer)
|
|
29693
|
+
return;
|
|
29694
|
+
this.clock.clearTimer(this.timer);
|
|
29695
|
+
this.timer = null;
|
|
29696
|
+
}
|
|
29697
|
+
}
|
|
29698
|
+
|
|
29602
29699
|
// src/daemon/createDaemon.ts
|
|
29603
29700
|
var WARMUP_BACKOFF_MS = [250, 500, 1000, 2000, 4000];
|
|
29604
29701
|
var WARMUP_CEILING_MS = 30000;
|
|
@@ -29755,6 +29852,10 @@ async function createDaemon(opts) {
|
|
|
29755
29852
|
let channelRef = null;
|
|
29756
29853
|
let managerRef = null;
|
|
29757
29854
|
let reminderSchedulerRef = null;
|
|
29855
|
+
const selfSleepScheduler = opts.onSelfSleep ? new DaemonSelfSleepScheduler({
|
|
29856
|
+
onSleep: opts.onSelfSleep,
|
|
29857
|
+
...opts.selfSleepClock ? { clock: opts.selfSleepClock } : {}
|
|
29858
|
+
}) : null;
|
|
29758
29859
|
const emitBotAuditEvent = (agentId, event, context) => {
|
|
29759
29860
|
channelRef?.reportBotAuditEvent?.({
|
|
29760
29861
|
type: "bot_audit_event",
|
|
@@ -30048,6 +30149,7 @@ async function createDaemon(opts) {
|
|
|
30048
30149
|
tickIntervalMs: opts.tickIntervalMs ?? 2000,
|
|
30049
30150
|
onAgentSession: (info) => void channel2.reportAgentSession(info),
|
|
30050
30151
|
onAgentActivity: (info) => {
|
|
30152
|
+
selfSleepScheduler?.observeAgentActivity(info.agentId, info.state === "running");
|
|
30051
30153
|
channel2.reportAgentActivity?.(info);
|
|
30052
30154
|
if (info.state === "starting" || info.state === "running") {
|
|
30053
30155
|
if (!typingHeartbeats.has(info.agentId)) {
|
|
@@ -30125,6 +30227,7 @@ async function createDaemon(opts) {
|
|
|
30125
30227
|
reportDiagnosticFailure: opts.reportDiagnosticFailure
|
|
30126
30228
|
}));
|
|
30127
30229
|
channel2.onWakeDesiredAdvance((cmd) => {
|
|
30230
|
+
selfSleepScheduler?.observeMessage();
|
|
30128
30231
|
reminderSchedulerRef?.observe(cmd.agentId, cmd.unreadNotice.channel, cmd.unreadNotice.latestSeq);
|
|
30129
30232
|
});
|
|
30130
30233
|
channel2.onCommand((cmd) => {
|
|
@@ -30149,6 +30252,7 @@ async function createDaemon(opts) {
|
|
|
30149
30252
|
});
|
|
30150
30253
|
channel2.connect();
|
|
30151
30254
|
await router.start();
|
|
30255
|
+
selfSleepScheduler?.start();
|
|
30152
30256
|
return {
|
|
30153
30257
|
isOpen: () => channel2.status === "open",
|
|
30154
30258
|
onOpen: (hook) => {
|
|
@@ -30158,6 +30262,7 @@ async function createDaemon(opts) {
|
|
|
30158
30262
|
},
|
|
30159
30263
|
proxyUrl: proxy.url,
|
|
30160
30264
|
stop: async () => {
|
|
30265
|
+
selfSleepScheduler?.stop();
|
|
30161
30266
|
reminderSchedulerRef?.clearAll();
|
|
30162
30267
|
for (const agentId of [...typingHeartbeats.keys()]) {
|
|
30163
30268
|
emitTypingStopsAndClear(agentId);
|
|
@@ -31974,6 +32079,10 @@ async function runPreparedDaemon(prepared, opts) {
|
|
|
31974
32079
|
onAuthRejected: () => {
|
|
31975
32080
|
log2.error("machine key rejected by server — is it correct / has it expired?");
|
|
31976
32081
|
shutdown(1);
|
|
32082
|
+
},
|
|
32083
|
+
onSelfSleep: () => {
|
|
32084
|
+
log2.info("daemon self-sleep threshold reached");
|
|
32085
|
+
shutdown(0);
|
|
31977
32086
|
}
|
|
31978
32087
|
});
|
|
31979
32088
|
} catch (error51) {
|
|
@@ -32984,14 +33093,16 @@ var LOCAL_MESSAGE_REMINDER_PATH2 = "/__alook/local/message-reminder";
|
|
|
32984
33093
|
var MIN_REMINDER_MS = 60000;
|
|
32985
33094
|
var MAX_REMINDER_MS = 24 * 60 * 60000;
|
|
32986
33095
|
function parseRemindAfter(value) {
|
|
33096
|
+
if (value === "0")
|
|
33097
|
+
return 0;
|
|
32987
33098
|
const match = /^(\d+)(m|h)$/.exec(value);
|
|
32988
33099
|
if (!match) {
|
|
32989
|
-
throw new Error("message send: --remind-after must be a positive integer followed by m or h (1m..24h)");
|
|
33100
|
+
throw new Error("message send: --remind-after must be 0 or a positive integer followed by m or h (1m..24h)");
|
|
32990
33101
|
}
|
|
32991
33102
|
const amount = Number(match[1]);
|
|
32992
33103
|
const milliseconds = amount * (match[2] === "h" ? 60 * 60000 : 60000);
|
|
32993
33104
|
if (!Number.isSafeInteger(milliseconds) || milliseconds < MIN_REMINDER_MS || milliseconds > MAX_REMINDER_MS) {
|
|
32994
|
-
throw new Error("message send: --remind-after must be between 1m and 24h");
|
|
33105
|
+
throw new Error("message send: --remind-after must be 0 or between 1m and 24h");
|
|
32995
33106
|
}
|
|
32996
33107
|
return milliseconds;
|
|
32997
33108
|
}
|
|
@@ -33126,7 +33237,8 @@ async function readLiteralInput(args) {
|
|
|
33126
33237
|
if (!stdin)
|
|
33127
33238
|
throw new CliError(`${command}: stdin is unavailable`);
|
|
33128
33239
|
if (stdin.isTTY === true) {
|
|
33129
|
-
|
|
33240
|
+
const suffix = fileOption ? `; use ${fileOption} in an interactive terminal` : "";
|
|
33241
|
+
throw new CliError(`${command}: --stdin requires piped input${suffix}`);
|
|
33130
33242
|
}
|
|
33131
33243
|
try {
|
|
33132
33244
|
const chunks = [];
|
|
@@ -33148,7 +33260,12 @@ async function readLiteralInput(args) {
|
|
|
33148
33260
|
}
|
|
33149
33261
|
return;
|
|
33150
33262
|
}
|
|
33263
|
+
var MALFORMED_ALOOK_HEREDOC_TAIL = /(^|\r?\n)(?:["']ALOOK_MESSAGE_[A-Z0-9_]+["']?|ALOOK_MESSAGE_[A-Z0-9_]+["'])(?:\r?\n)?$/;
|
|
33264
|
+
function stripMalformedAlookHeredocTail(input) {
|
|
33265
|
+
return input.replace(MALFORMED_ALOOK_HEREDOC_TAIL, "$1");
|
|
33266
|
+
}
|
|
33151
33267
|
var CLIENT_MAX_ATTACHMENT_BYTES = 26214400;
|
|
33268
|
+
var CLIENT_MAX_MESSAGE_BODY_BYTES = 1024;
|
|
33152
33269
|
function contentTypeFromFilename(filename) {
|
|
33153
33270
|
const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase();
|
|
33154
33271
|
switch (ext) {
|
|
@@ -33208,24 +33325,28 @@ async function sendWithRetry(api2, req) {
|
|
|
33208
33325
|
}
|
|
33209
33326
|
async function cmdMessageSend(opts, stdin) {
|
|
33210
33327
|
const remindAfterFlag = opts.remindAfter;
|
|
33211
|
-
const remindAfterMs =
|
|
33328
|
+
const remindAfterMs = parseRemindAfter(remindAfterFlag);
|
|
33212
33329
|
const api2 = getApi();
|
|
33213
33330
|
const agent2 = agentId(opts);
|
|
33214
33331
|
const channel2 = opts.target;
|
|
33215
33332
|
if (!channel2)
|
|
33216
33333
|
throw new CliError("message send: --target <ref> is required (e.g. /demo-workspace#1234/general)");
|
|
33217
|
-
const
|
|
33218
|
-
const text2 = await readLiteralInput({
|
|
33334
|
+
const literalText = await readLiteralInput({
|
|
33219
33335
|
command: "message send",
|
|
33220
33336
|
stdinSelected: opts.stdin === true,
|
|
33221
|
-
stdin
|
|
33222
|
-
filePath: fileFlag,
|
|
33223
|
-
fileOption: "--file <path>"
|
|
33337
|
+
stdin
|
|
33224
33338
|
});
|
|
33339
|
+
const text2 = stripMalformedAlookHeredocTail(literalText ?? "");
|
|
33340
|
+
const textBytes = Buffer.byteLength(text2, "utf8");
|
|
33341
|
+
if (textBytes > CLIENT_MAX_MESSAGE_BODY_BYTES) {
|
|
33342
|
+
throw new CliError(`message send: --stdin body is ${textBytes} bytes; max ${CLIENT_MAX_MESSAGE_BODY_BYTES}. Rewrite it before retrying.
|
|
33343
|
+
|
|
33344
|
+
${MESSAGE_SEND_STDIN_POLICY}`);
|
|
33345
|
+
}
|
|
33225
33346
|
const attachmentIds = Array.isArray(opts.attachment) ? opts.attachment : [];
|
|
33226
|
-
const hasText =
|
|
33347
|
+
const hasText = text2.trim().length > 0;
|
|
33227
33348
|
if (!hasText && attachmentIds.length === 0) {
|
|
33228
|
-
throw new CliError("message send: --stdin
|
|
33349
|
+
throw new CliError("message send: --stdin must contain text unless --attachment <id> is present");
|
|
33229
33350
|
}
|
|
33230
33351
|
let replyToSeq;
|
|
33231
33352
|
const replyFlag = opts.reply;
|
|
@@ -33251,8 +33372,6 @@ async function cmdMessageSend(opts, stdin) {
|
|
|
33251
33372
|
throw new CliError(`channel not aligned: ${res.unreadCount} unread message(s) in ${channel2} (latest #${res.latestSeq}). Run \`alook inbox pull\` and READ the new messages before deciding whether to resend, adjust, or skip your message.`);
|
|
33252
33373
|
}
|
|
33253
33374
|
const sent = `${res.message.channel}${res.message.seq}`;
|
|
33254
|
-
if (remindAfterMs === undefined)
|
|
33255
|
-
return { sent };
|
|
33256
33375
|
const seqText = res.message.seq.replace(/^#/, "");
|
|
33257
33376
|
const sentSeq = Number(seqText);
|
|
33258
33377
|
if (!/^\d+$/.test(seqText) || !Number.isSafeInteger(sentSeq) || sentSeq < 1) {
|
|
@@ -33586,7 +33705,7 @@ function buildProgram(stdin) {
|
|
|
33586
33705
|
}).option("--agent <id>", "agent identity (or ALOOK_AGENT_ID env)");
|
|
33587
33706
|
const message2 = program.command("message").description("message operations").exitOverride();
|
|
33588
33707
|
message2.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
33589
|
-
message2.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace#1234/general)").
|
|
33708
|
+
message2.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace#1234/general)").requiredOption("--stdin", "read the required UTF-8 message body from non-TTY stdin (max 1 KiB)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).option("--reply <seq>", 'reply to a message by its seq in --target (e.g. "#37" or 37)').requiredOption("--remind-after <0|Nm|Nh>", "required idle follow-up: 0 disables; 1m..24h arms/resets one same-scope timer; a newer message or daemon restart cancels it").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
33590
33709
|
const localOpts = this.opts();
|
|
33591
33710
|
const globalOpts = program.opts();
|
|
33592
33711
|
const result = await cmdMessageSend({ ...globalOpts, ...localOpts }, stdin);
|
package/dist/index.js
CHANGED
|
@@ -6891,6 +6891,16 @@ function runtimeModelName(config) {
|
|
|
6891
6891
|
|
|
6892
6892
|
// src/drivers/systemPrompt.ts
|
|
6893
6893
|
var CLI = "$ALOOK_CLI";
|
|
6894
|
+
var MESSAGE_SEND_STDIN_POLICY = [
|
|
6895
|
+
"`--stdin` is required and limited to 1 KiB of UTF-8. Write it as social language a person " + "with ADHD can scan without effort:",
|
|
6896
|
+
"",
|
|
6897
|
+
"- Lead with the point, result, decision, or one concrete ask.",
|
|
6898
|
+
"- Keep one message to one topic; suppress tangents and repeated recap.",
|
|
6899
|
+
"- Use at most five short items when a list helps.",
|
|
6900
|
+
"- Drop preambles, play-by-play, and closing pleasantries.",
|
|
6901
|
+
"- Put exact plans, reviews, evidence, logs, and long technical detail in a Markdown attachment. " + "The message body carries only the short summary and next action."
|
|
6902
|
+
].join(`
|
|
6903
|
+
`);
|
|
6894
6904
|
function identitySection(config) {
|
|
6895
6905
|
const parts = ["## Identity", ""];
|
|
6896
6906
|
const name = config.agentName ?? "a member of the household";
|
|
@@ -6921,9 +6931,9 @@ function cliCommandsSection() {
|
|
|
6921
6931
|
"",
|
|
6922
6932
|
"### Messaging",
|
|
6923
6933
|
"",
|
|
6924
|
-
`1. \`${CLI} inbox pull\` — fetch unread messages
|
|
6925
|
-
`2. \`${CLI} message send\` — send to a channel, DM, or thread. ` + `
|
|
6926
|
-
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted
|
|
6934
|
+
`1. \`${CLI} inbox pull\` — fetch unread messages; \`--no-ack\` peeks without advancing.`,
|
|
6935
|
+
`2. \`${CLI} message send --target <ref> --remind-after <0|Nm|Nh> --stdin\` — send to a ` + `channel, DM, or thread. ` + `The body is required through \`--stdin\` and limited to 1 KiB of UTF-8. ` + `Attach uploaded files with \`--attachment <id>\` (repeatable, order matters). ` + `\`--remind-after\` accepts \`0\`, or a whole-number duration from \`1m\` to \`24h\`.`,
|
|
6936
|
+
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted.`,
|
|
6927
6937
|
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
6928
6938
|
`5. \`${CLI} message emoji --target <ref> --emoji <e>\` — react with a single emoji. ` + `Works on channel messages (\`/<server>/<channel>#N\`), DM messages ` + `(\`/.dm/<peer>#N\`), and thread-reply messages (\`/<server>/<channel>/#N#M\`).`,
|
|
6929
6939
|
`6. \`${CLI} message mark set --target <full-message-ref>\` — persist a message as outstanding work.`,
|
|
@@ -6953,7 +6963,7 @@ function cliCommandsSection() {
|
|
|
6953
6963
|
"",
|
|
6954
6964
|
"### Context Lifecycle",
|
|
6955
6965
|
"",
|
|
6956
|
-
`1. \`${CLI} nap --handoff <file>\` — reset
|
|
6966
|
+
`1. \`${CLI} nap --handoff <file>\` — reset the current session from a required handoff file.`,
|
|
6957
6967
|
"",
|
|
6958
6968
|
"### Output format",
|
|
6959
6969
|
"",
|
|
@@ -6969,14 +6979,29 @@ function messagingSection() {
|
|
|
6969
6979
|
"",
|
|
6970
6980
|
"### Sending & receiving",
|
|
6971
6981
|
"",
|
|
6972
|
-
"You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying
|
|
6982
|
+
"You can initiate conversations — send to any channel or DM someone directly. You're not " + "limited to replying; the same sending rules apply either way.",
|
|
6983
|
+
"",
|
|
6984
|
+
"#### Message body",
|
|
6985
|
+
"",
|
|
6986
|
+
MESSAGE_SEND_STDIN_POLICY,
|
|
6987
|
+
"",
|
|
6988
|
+
"#### Follow up when a conversation goes quiet",
|
|
6989
|
+
"",
|
|
6990
|
+
"`--remind-after T` is required on every send and controls whether a local follow-up is " + "armed for the same channel, thread, or DM.",
|
|
6973
6991
|
"",
|
|
6974
|
-
"-
|
|
6975
|
-
"
|
|
6992
|
+
"- Use `1m` to `24h` when silence would leave work unfinished — for example, after a question, " + "approval request, handoff, or blocker. If no newer message arrives by T, you will be reminded " + "to return; a newer message or daemon restart cancels the timer.",
|
|
6993
|
+
"- Use `0` only when no later action depends on a reply. It disables the timer.",
|
|
6976
6994
|
"",
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6995
|
+
"Example: `--remind-after 5m` asks for a follow-up after five quiet minutes.",
|
|
6996
|
+
"",
|
|
6997
|
+
"#### Sending mechanics",
|
|
6998
|
+
"",
|
|
6999
|
+
`- Send body: use \`${CLI} message send --target <ref> --remind-after T --stdin\` with the ` + "quoted-heredoc form under *Message formatting*.",
|
|
7000
|
+
`- Long detail: upload it with \`${CLI} message attachment upload --target <ref> --file <path>.md\`; ` + "add the returned id as `--attachment <id>` on the short stdin send.",
|
|
7001
|
+
`- Cite a specific message: add \`--reply "#37"\` — \`--reply\` takes the \`#N\` seq ` + "(within `--target`) of the message you're answering.",
|
|
7002
|
+
"",
|
|
7003
|
+
"Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, read history (below) or DM the relevant people.",
|
|
7004
|
+
"Write every message body in the stdin/heredoc block; never place it directly on the command line.",
|
|
6980
7005
|
"",
|
|
6981
7006
|
"### Context refs",
|
|
6982
7007
|
"",
|
|
@@ -7010,14 +7035,14 @@ function messagingSection() {
|
|
|
7010
7035
|
"",
|
|
7011
7036
|
"### Message formatting",
|
|
7012
7037
|
"",
|
|
7013
|
-
"Alook
|
|
7038
|
+
"Alook specially renders refs and mentions in message bodies. Write them as plain text, not " + "inside backticks.",
|
|
7014
7039
|
"",
|
|
7015
|
-
"- **Context refs** —
|
|
7040
|
+
"- **Context refs** — write the full refs from *Context refs* above so they stay " + "clickable. Never paste a private DM ref into a server channel.",
|
|
7016
7041
|
"- **Mentions** — `@name#NNNN` calls that person's attention specifically. In a private " + "channel, first verify they " + `are a member with \`${CLI} channel member --channel <ref>\` before mentioning them.`,
|
|
7017
7042
|
"",
|
|
7018
7043
|
"```bash",
|
|
7019
7044
|
"# Choose a fresh quoted delimiter that does not occur as a standalone line in the body.",
|
|
7020
|
-
`${CLI} message send --target "/demo#1234/general" --stdin <<'ALOOK_MESSAGE_7F3C'`,
|
|
7045
|
+
`${CLI} message send --target "/demo#1234/general" --remind-after 5m --stdin <<'ALOOK_MESSAGE_7F3C'`,
|
|
7021
7046
|
"@alice#0001 Please review /demo#1234/general#42",
|
|
7022
7047
|
"ALOOK_MESSAGE_7F3C",
|
|
7023
7048
|
"```",
|
|
@@ -7030,7 +7055,7 @@ function messagingSection() {
|
|
|
7030
7055
|
"```",
|
|
7031
7056
|
"",
|
|
7032
7057
|
"`channel` is the reply ref. `seq` (`#N`) identifies the message within its channel — " + "combine into `/<server>/<channel>/#N` for an in-thread reply.",
|
|
7033
|
-
"`content.replyTo` (`{seq, sender}`)
|
|
7058
|
+
"`content.replyTo` (`{seq, sender}`) identifies the message being replied to.",
|
|
7034
7059
|
"`hint` is present when the containing surface changes how you should act. Follow it."
|
|
7035
7060
|
].join(`
|
|
7036
7061
|
`);
|
|
@@ -7043,15 +7068,15 @@ function channelTypesSection() {
|
|
|
7043
7068
|
"",
|
|
7044
7069
|
"### Text channels",
|
|
7045
7070
|
"",
|
|
7046
|
-
"- A text channel is a linear conversation. Send
|
|
7071
|
+
"- A text channel is a linear conversation. Send with `message send --target " + "/<server>/<channel>`.",
|
|
7047
7072
|
"- A text-channel message may have a side thread at `/<server>/<channel>/#N`. Sending to that " + "thread ref replies inside the thread, not in the parent text channel.",
|
|
7048
7073
|
"",
|
|
7049
7074
|
"### Forum channels",
|
|
7050
7075
|
"",
|
|
7051
7076
|
"- A forum is a collection of posts, not one linear conversation.",
|
|
7052
7077
|
"- Each top-level message in a forum is a post title. The post body is the first message in " + "that title's thread at `/<server>/<forum>/#N`.",
|
|
7053
|
-
"- To participate in
|
|
7054
|
-
"- To publish
|
|
7078
|
+
"- To participate in a post, use `message send --target /<server>/<forum>/#N`.",
|
|
7079
|
+
"- To publish a post, use `message send --target /<server>/<forum>` for its title, then " + "`message send --target /<server>/<forum>/#N` for the body."
|
|
7055
7080
|
].join(`
|
|
7056
7081
|
`);
|
|
7057
7082
|
}
|
|
@@ -7079,13 +7104,7 @@ function utilsSection() {
|
|
|
7079
7104
|
"",
|
|
7080
7105
|
"### Join a new server",
|
|
7081
7106
|
"",
|
|
7082
|
-
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
|
|
7083
|
-
"",
|
|
7084
|
-
"### Follow up when a conversation goes quiet",
|
|
7085
|
-
"",
|
|
7086
|
-
`Use \`message send --remind-after <duration>\` when you send something that may need a later ` + "follow-up — for example, a question, approval request, handoff, or blocker — and silence would " + "leave the work unfinished. If no newer message appears in that channel, thread, or DM during " + "the duration, you'll receive a reminder to return and decide what to do next. Don't add it to " + "ordinary messages that need no follow-up.",
|
|
7087
|
-
"",
|
|
7088
|
-
`Example: ${CLI} message send --target "/demo#1234/team" --remind-after 1m --file ./message.md`
|
|
7107
|
+
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
|
|
7089
7108
|
].join(`
|
|
7090
7109
|
`);
|
|
7091
7110
|
}
|
|
@@ -7093,11 +7112,10 @@ function criticalRulesSection() {
|
|
|
7093
7112
|
return [
|
|
7094
7113
|
"## Critical rules",
|
|
7095
7114
|
"",
|
|
7096
|
-
`- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something,
|
|
7115
|
+
`- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, share it through \`${CLI}\`, uploading a file when needed.`,
|
|
7097
7116
|
"- **Never expose tokens, keys, or secrets.** Redact credential-like strings from tool output " + "before sharing.",
|
|
7098
7117
|
"- **Match the sender's language.** Reply in the language they wrote in.",
|
|
7099
|
-
"- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend."
|
|
7100
|
-
"- **Finish in-flight work before stopping.** Don't leave anything half-handled. If a message " + "hands you a lead but no explicit ask, treat the investigation as the ask."
|
|
7118
|
+
"- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend."
|
|
7101
7119
|
].join(`
|
|
7102
7120
|
`);
|
|
7103
7121
|
}
|
|
@@ -7105,7 +7123,7 @@ function executionModelSection() {
|
|
|
7105
7123
|
return [
|
|
7106
7124
|
"## How you work — async, not turn-based",
|
|
7107
7125
|
"",
|
|
7108
|
-
"Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. Stop only when all of it is done.",
|
|
7126
|
+
"Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. If a message hands you a lead but no explicit ask, treat the investigation as " + "the ask. Stop only when all of it is done.",
|
|
7109
7127
|
"",
|
|
7110
7128
|
"On wake, restore durable context from `memory.md` and the context timeline, then pull your inbox. " + "Follow *Outstanding work marks* below before taking new work.",
|
|
7111
7129
|
"",
|
|
@@ -25792,7 +25810,7 @@ function parseLocalMessageReminderBody(body, agentId) {
|
|
|
25792
25810
|
return null;
|
|
25793
25811
|
if (!Number.isSafeInteger(record4.sentSeq) || record4.sentSeq < 1)
|
|
25794
25812
|
return null;
|
|
25795
|
-
if (!Number.isSafeInteger(record4.remindAfterMs) || record4.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record4.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
|
|
25813
|
+
if (!Number.isSafeInteger(record4.remindAfterMs) || record4.remindAfterMs !== 0 && record4.remindAfterMs < LOCAL_MESSAGE_REMINDER_MIN_MS || record4.remindAfterMs > LOCAL_MESSAGE_REMINDER_MAX_MS)
|
|
25796
25814
|
return null;
|
|
25797
25815
|
return {
|
|
25798
25816
|
agentId,
|
|
@@ -27630,6 +27648,10 @@ class MessageReminderScheduler {
|
|
|
27630
27648
|
}
|
|
27631
27649
|
arm(input) {
|
|
27632
27650
|
const key = reminderKey(input.agentId, input.channel);
|
|
27651
|
+
if (input.remindAfterMs === 0) {
|
|
27652
|
+
this.clearReminder(key);
|
|
27653
|
+
return { armed: false, reason: "disabled" };
|
|
27654
|
+
}
|
|
27633
27655
|
const latest = this.latestObservedSeq.get(key);
|
|
27634
27656
|
if (latest !== undefined && latest > input.sentSeq) {
|
|
27635
27657
|
return { armed: false, reason: "newer_message_observed" };
|
|
@@ -27857,6 +27879,78 @@ function createDaemonAgentDriverHost(ctx, onRawLine) {
|
|
|
27857
27879
|
};
|
|
27858
27880
|
}
|
|
27859
27881
|
|
|
27882
|
+
// src/daemon/daemonSelfSleep.ts
|
|
27883
|
+
var DAEMON_SELF_SLEEP_TIMEOUT_MS = 15 * 24 * 60 * 60 * 1000;
|
|
27884
|
+
var systemClock = {
|
|
27885
|
+
setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
27886
|
+
clearTimer: (timer) => clearTimeout(timer)
|
|
27887
|
+
};
|
|
27888
|
+
|
|
27889
|
+
class DaemonSelfSleepScheduler {
|
|
27890
|
+
opts;
|
|
27891
|
+
clock;
|
|
27892
|
+
workingAgents = new Set;
|
|
27893
|
+
timer = null;
|
|
27894
|
+
generation = 0;
|
|
27895
|
+
started = false;
|
|
27896
|
+
stopped = false;
|
|
27897
|
+
constructor(opts) {
|
|
27898
|
+
this.opts = opts;
|
|
27899
|
+
this.clock = opts.clock ?? systemClock;
|
|
27900
|
+
}
|
|
27901
|
+
start() {
|
|
27902
|
+
if (this.started || this.stopped)
|
|
27903
|
+
return;
|
|
27904
|
+
this.started = true;
|
|
27905
|
+
this.arm();
|
|
27906
|
+
}
|
|
27907
|
+
observeMessage() {
|
|
27908
|
+
if (!this.started || this.stopped)
|
|
27909
|
+
return;
|
|
27910
|
+
this.arm();
|
|
27911
|
+
}
|
|
27912
|
+
observeAgentActivity(agentId, working) {
|
|
27913
|
+
if (this.stopped)
|
|
27914
|
+
return;
|
|
27915
|
+
if (working) {
|
|
27916
|
+
this.workingAgents.add(agentId);
|
|
27917
|
+
if (this.started)
|
|
27918
|
+
this.cancel();
|
|
27919
|
+
return;
|
|
27920
|
+
}
|
|
27921
|
+
if (this.workingAgents.delete(agentId) && this.started && this.workingAgents.size === 0)
|
|
27922
|
+
this.arm();
|
|
27923
|
+
}
|
|
27924
|
+
stop() {
|
|
27925
|
+
if (this.stopped)
|
|
27926
|
+
return;
|
|
27927
|
+
this.stopped = true;
|
|
27928
|
+
this.workingAgents.clear();
|
|
27929
|
+
this.cancel();
|
|
27930
|
+
}
|
|
27931
|
+
arm() {
|
|
27932
|
+
this.cancel();
|
|
27933
|
+
if (this.workingAgents.size > 0)
|
|
27934
|
+
return;
|
|
27935
|
+
const generation = this.generation;
|
|
27936
|
+
this.timer = this.clock.setTimer(() => {
|
|
27937
|
+
if (this.stopped || this.generation !== generation || this.workingAgents.size > 0)
|
|
27938
|
+
return;
|
|
27939
|
+
this.timer = null;
|
|
27940
|
+
this.generation += 1;
|
|
27941
|
+
this.opts.onSleep();
|
|
27942
|
+
}, DAEMON_SELF_SLEEP_TIMEOUT_MS);
|
|
27943
|
+
this.timer.unref?.();
|
|
27944
|
+
}
|
|
27945
|
+
cancel() {
|
|
27946
|
+
this.generation += 1;
|
|
27947
|
+
if (!this.timer)
|
|
27948
|
+
return;
|
|
27949
|
+
this.clock.clearTimer(this.timer);
|
|
27950
|
+
this.timer = null;
|
|
27951
|
+
}
|
|
27952
|
+
}
|
|
27953
|
+
|
|
27860
27954
|
// src/daemon/createDaemon.ts
|
|
27861
27955
|
var WARMUP_BACKOFF_MS = [250, 500, 1000, 2000, 4000];
|
|
27862
27956
|
var WARMUP_CEILING_MS = 30000;
|
|
@@ -28013,6 +28107,10 @@ async function createDaemon(opts) {
|
|
|
28013
28107
|
let channelRef = null;
|
|
28014
28108
|
let managerRef = null;
|
|
28015
28109
|
let reminderSchedulerRef = null;
|
|
28110
|
+
const selfSleepScheduler = opts.onSelfSleep ? new DaemonSelfSleepScheduler({
|
|
28111
|
+
onSleep: opts.onSelfSleep,
|
|
28112
|
+
...opts.selfSleepClock ? { clock: opts.selfSleepClock } : {}
|
|
28113
|
+
}) : null;
|
|
28016
28114
|
const emitBotAuditEvent = (agentId, event, context) => {
|
|
28017
28115
|
channelRef?.reportBotAuditEvent?.({
|
|
28018
28116
|
type: "bot_audit_event",
|
|
@@ -28306,6 +28404,7 @@ async function createDaemon(opts) {
|
|
|
28306
28404
|
tickIntervalMs: opts.tickIntervalMs ?? 2000,
|
|
28307
28405
|
onAgentSession: (info) => void channel2.reportAgentSession(info),
|
|
28308
28406
|
onAgentActivity: (info) => {
|
|
28407
|
+
selfSleepScheduler?.observeAgentActivity(info.agentId, info.state === "running");
|
|
28309
28408
|
channel2.reportAgentActivity?.(info);
|
|
28310
28409
|
if (info.state === "starting" || info.state === "running") {
|
|
28311
28410
|
if (!typingHeartbeats.has(info.agentId)) {
|
|
@@ -28383,6 +28482,7 @@ async function createDaemon(opts) {
|
|
|
28383
28482
|
reportDiagnosticFailure: opts.reportDiagnosticFailure
|
|
28384
28483
|
}));
|
|
28385
28484
|
channel2.onWakeDesiredAdvance((cmd) => {
|
|
28485
|
+
selfSleepScheduler?.observeMessage();
|
|
28386
28486
|
reminderSchedulerRef?.observe(cmd.agentId, cmd.unreadNotice.channel, cmd.unreadNotice.latestSeq);
|
|
28387
28487
|
});
|
|
28388
28488
|
channel2.onCommand((cmd) => {
|
|
@@ -28407,6 +28507,7 @@ async function createDaemon(opts) {
|
|
|
28407
28507
|
});
|
|
28408
28508
|
channel2.connect();
|
|
28409
28509
|
await router.start();
|
|
28510
|
+
selfSleepScheduler?.start();
|
|
28410
28511
|
return {
|
|
28411
28512
|
isOpen: () => channel2.status === "open",
|
|
28412
28513
|
onOpen: (hook) => {
|
|
@@ -28416,6 +28517,7 @@ async function createDaemon(opts) {
|
|
|
28416
28517
|
},
|
|
28417
28518
|
proxyUrl: proxy.url,
|
|
28418
28519
|
stop: async () => {
|
|
28520
|
+
selfSleepScheduler?.stop();
|
|
28419
28521
|
reminderSchedulerRef?.clearAll();
|
|
28420
28522
|
for (const agentId of [...typingHeartbeats.keys()]) {
|
|
28421
28523
|
emitTypingStopsAndClear(agentId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alook/daemon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Alook agent daemon — host-side runtime backend, process manager, credential proxy, and control plane.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/alookai/alook#readme",
|