@asermax/tachikoma 2.0.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 +64 -0
- package/dist/agent/adapter.d.ts +8 -0
- package/dist/agent/adapter.js +86 -0
- package/dist/agent/adapter.js.map +1 -0
- package/dist/agent/manager.d.ts +35 -0
- package/dist/agent/manager.js +76 -0
- package/dist/agent/manager.js.map +1 -0
- package/dist/agent/models.d.ts +46 -0
- package/dist/agent/models.js +96 -0
- package/dist/agent/models.js.map +1 -0
- package/dist/agent/side-run.d.ts +42 -0
- package/dist/agent/side-run.js +83 -0
- package/dist/agent/side-run.js.map +1 -0
- package/dist/app.d.ts +5 -0
- package/dist/app.js +79 -0
- package/dist/app.js.map +1 -0
- package/dist/channels/types.d.ts +37 -0
- package/dist/channels/types.js +5 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/config/default-template.d.ts +1 -0
- package/dist/config/default-template.js +49 -0
- package/dist/config/default-template.js.map +1 -0
- package/dist/config/load.d.ts +8 -0
- package/dist/config/load.js +28 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/parse.d.ts +5 -0
- package/dist/config/parse.js +18 -0
- package/dist/config/parse.js.map +1 -0
- package/dist/config/schema.d.ts +29 -0
- package/dist/config/schema.js +35 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/coordinator.d.ts +54 -0
- package/dist/coordinator.js +344 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/db/core-schema.d.ts +250 -0
- package/dist/db/core-schema.js +19 -0
- package/dist/db/core-schema.js.map +1 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +16 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +4 -0
- package/dist/db/schema.js +7 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/state.d.ts +10 -0
- package/dist/db/state.js +36 -0
- package/dist/db/state.js.map +1 -0
- package/dist/domain/agent-events.d.ts +26 -0
- package/dist/domain/agent-events.js +2 -0
- package/dist/domain/agent-events.js.map +1 -0
- package/dist/domain/message.d.ts +25 -0
- package/dist/domain/message.js +17 -0
- package/dist/domain/message.js.map +1 -0
- package/dist/events.d.ts +9 -0
- package/dist/events.js +27 -0
- package/dist/events.js.map +1 -0
- package/dist/extensions/api.d.ts +118 -0
- package/dist/extensions/api.js +7 -0
- package/dist/extensions/api.js.map +1 -0
- package/dist/extensions/boundary/detector.d.ts +20 -0
- package/dist/extensions/boundary/detector.js +57 -0
- package/dist/extensions/boundary/detector.js.map +1 -0
- package/dist/extensions/boundary/idle.d.ts +10 -0
- package/dist/extensions/boundary/idle.js +28 -0
- package/dist/extensions/boundary/idle.js.map +1 -0
- package/dist/extensions/boundary/index.d.ts +12 -0
- package/dist/extensions/boundary/index.js +65 -0
- package/dist/extensions/boundary/index.js.map +1 -0
- package/dist/extensions/boundary/summary.d.ts +5 -0
- package/dist/extensions/boundary/summary.js +33 -0
- package/dist/extensions/boundary/summary.js.map +1 -0
- package/dist/extensions/commands/index.d.ts +7 -0
- package/dist/extensions/commands/index.js +21 -0
- package/dist/extensions/commands/index.js.map +1 -0
- package/dist/extensions/context/index.d.ts +7 -0
- package/dist/extensions/context/index.js +65 -0
- package/dist/extensions/context/index.js.map +1 -0
- package/dist/extensions/context/processor.d.ts +27 -0
- package/dist/extensions/context/processor.js +228 -0
- package/dist/extensions/context/processor.js.map +1 -0
- package/dist/extensions/detached-processes/index.d.ts +12 -0
- package/dist/extensions/detached-processes/index.js +51 -0
- package/dist/extensions/detached-processes/index.js.map +1 -0
- package/dist/extensions/detached-processes/limits.d.ts +27 -0
- package/dist/extensions/detached-processes/limits.js +55 -0
- package/dist/extensions/detached-processes/limits.js.map +1 -0
- package/dist/extensions/detached-processes/output.d.ts +2 -0
- package/dist/extensions/detached-processes/output.js +26 -0
- package/dist/extensions/detached-processes/output.js.map +1 -0
- package/dist/extensions/detached-processes/reconcile.d.ts +31 -0
- package/dist/extensions/detached-processes/reconcile.js +71 -0
- package/dist/extensions/detached-processes/reconcile.js.map +1 -0
- package/dist/extensions/detached-processes/repository.d.ts +33 -0
- package/dist/extensions/detached-processes/repository.js +62 -0
- package/dist/extensions/detached-processes/repository.js.map +1 -0
- package/dist/extensions/detached-processes/schema.d.ts +252 -0
- package/dist/extensions/detached-processes/schema.js +23 -0
- package/dist/extensions/detached-processes/schema.js.map +1 -0
- package/dist/extensions/detached-processes/spawn.d.ts +40 -0
- package/dist/extensions/detached-processes/spawn.js +137 -0
- package/dist/extensions/detached-processes/spawn.js.map +1 -0
- package/dist/extensions/detached-processes/tools.d.ts +41 -0
- package/dist/extensions/detached-processes/tools.js +243 -0
- package/dist/extensions/detached-processes/tools.js.map +1 -0
- package/dist/extensions/detached-processes/watcher.d.ts +7 -0
- package/dist/extensions/detached-processes/watcher.js +19 -0
- package/dist/extensions/detached-processes/watcher.js.map +1 -0
- package/dist/extensions/external/index.d.ts +11 -0
- package/dist/extensions/external/index.js +40 -0
- package/dist/extensions/external/index.js.map +1 -0
- package/dist/extensions/external/installs.d.ts +39 -0
- package/dist/extensions/external/installs.js +98 -0
- package/dist/extensions/external/installs.js.map +1 -0
- package/dist/extensions/external/loader.d.ts +19 -0
- package/dist/extensions/external/loader.js +70 -0
- package/dist/extensions/external/loader.js.map +1 -0
- package/dist/extensions/external/tools.d.ts +17 -0
- package/dist/extensions/external/tools.js +112 -0
- package/dist/extensions/external/tools.js.map +1 -0
- package/dist/extensions/git/commit.d.ts +19 -0
- package/dist/extensions/git/commit.js +44 -0
- package/dist/extensions/git/commit.js.map +1 -0
- package/dist/extensions/git/git.d.ts +11 -0
- package/dist/extensions/git/git.js +29 -0
- package/dist/extensions/git/git.js.map +1 -0
- package/dist/extensions/git/hooks.d.ts +10 -0
- package/dist/extensions/git/hooks.js +88 -0
- package/dist/extensions/git/hooks.js.map +1 -0
- package/dist/extensions/git/index.d.ts +11 -0
- package/dist/extensions/git/index.js +28 -0
- package/dist/extensions/git/index.js.map +1 -0
- package/dist/extensions/git/processor.d.ts +13 -0
- package/dist/extensions/git/processor.js +52 -0
- package/dist/extensions/git/processor.js.map +1 -0
- package/dist/extensions/git/sync.d.ts +44 -0
- package/dist/extensions/git/sync.js +189 -0
- package/dist/extensions/git/sync.js.map +1 -0
- package/dist/extensions/git/tools.d.ts +21 -0
- package/dist/extensions/git/tools.js +101 -0
- package/dist/extensions/git/tools.js.map +1 -0
- package/dist/extensions/host.d.ts +31 -0
- package/dist/extensions/host.js +75 -0
- package/dist/extensions/host.js.map +1 -0
- package/dist/extensions/index.d.ts +3 -0
- package/dist/extensions/index.js +32 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/extensions/memory/archive.d.ts +8 -0
- package/dist/extensions/memory/archive.js +46 -0
- package/dist/extensions/memory/archive.js.map +1 -0
- package/dist/extensions/memory/dates.d.ts +2 -0
- package/dist/extensions/memory/dates.js +7 -0
- package/dist/extensions/memory/dates.js.map +1 -0
- package/dist/extensions/memory/extraction.d.ts +17 -0
- package/dist/extensions/memory/extraction.js +218 -0
- package/dist/extensions/memory/extraction.js.map +1 -0
- package/dist/extensions/memory/index.d.ts +20 -0
- package/dist/extensions/memory/index.js +67 -0
- package/dist/extensions/memory/index.js.map +1 -0
- package/dist/extensions/memory/indexes.d.ts +14 -0
- package/dist/extensions/memory/indexes.js +64 -0
- package/dist/extensions/memory/indexes.js.map +1 -0
- package/dist/extensions/memory/layout.d.ts +20 -0
- package/dist/extensions/memory/layout.js +79 -0
- package/dist/extensions/memory/layout.js.map +1 -0
- package/dist/extensions/memory/maintenance.d.ts +21 -0
- package/dist/extensions/memory/maintenance.js +357 -0
- package/dist/extensions/memory/maintenance.js.map +1 -0
- package/dist/extensions/memory/prompts.d.ts +8 -0
- package/dist/extensions/memory/prompts.js +125 -0
- package/dist/extensions/memory/prompts.js.map +1 -0
- package/dist/extensions/memory/transcript.d.ts +18 -0
- package/dist/extensions/memory/transcript.js +79 -0
- package/dist/extensions/memory/transcript.js.map +1 -0
- package/dist/extensions/notifications/format.d.ts +5 -0
- package/dist/extensions/notifications/format.js +17 -0
- package/dist/extensions/notifications/format.js.map +1 -0
- package/dist/extensions/notifications/index.d.ts +13 -0
- package/dist/extensions/notifications/index.js +29 -0
- package/dist/extensions/notifications/index.js.map +1 -0
- package/dist/extensions/notifications/payload.d.ts +22 -0
- package/dist/extensions/notifications/payload.js +29 -0
- package/dist/extensions/notifications/payload.js.map +1 -0
- package/dist/extensions/notifications/router.d.ts +29 -0
- package/dist/extensions/notifications/router.js +55 -0
- package/dist/extensions/notifications/router.js.map +1 -0
- package/dist/extensions/notifications/tools.d.ts +12 -0
- package/dist/extensions/notifications/tools.js +41 -0
- package/dist/extensions/notifications/tools.js.map +1 -0
- package/dist/extensions/projects/context-provider.d.ts +9 -0
- package/dist/extensions/projects/context-provider.js +37 -0
- package/dist/extensions/projects/context-provider.js.map +1 -0
- package/dist/extensions/projects/git.d.ts +28 -0
- package/dist/extensions/projects/git.js +91 -0
- package/dist/extensions/projects/git.js.map +1 -0
- package/dist/extensions/projects/hooks.d.ts +7 -0
- package/dist/extensions/projects/hooks.js +42 -0
- package/dist/extensions/projects/hooks.js.map +1 -0
- package/dist/extensions/projects/index.d.ts +11 -0
- package/dist/extensions/projects/index.js +30 -0
- package/dist/extensions/projects/index.js.map +1 -0
- package/dist/extensions/projects/processor.d.ts +13 -0
- package/dist/extensions/projects/processor.js +63 -0
- package/dist/extensions/projects/processor.js.map +1 -0
- package/dist/extensions/projects/tools.d.ts +21 -0
- package/dist/extensions/projects/tools.js +118 -0
- package/dist/extensions/projects/tools.js.map +1 -0
- package/dist/extensions/registrations.d.ts +21 -0
- package/dist/extensions/registrations.js +12 -0
- package/dist/extensions/registrations.js.map +1 -0
- package/dist/extensions/repl/index.d.ts +2 -0
- package/dist/extensions/repl/index.js +85 -0
- package/dist/extensions/repl/index.js.map +1 -0
- package/dist/extensions/skills/agents.d.ts +17 -0
- package/dist/extensions/skills/agents.js +77 -0
- package/dist/extensions/skills/agents.js.map +1 -0
- package/dist/extensions/skills/delegate.d.ts +22 -0
- package/dist/extensions/skills/delegate.js +54 -0
- package/dist/extensions/skills/delegate.js.map +1 -0
- package/dist/extensions/skills/index.d.ts +11 -0
- package/dist/extensions/skills/index.js +43 -0
- package/dist/extensions/skills/index.js.map +1 -0
- package/dist/extensions/skills/reload.d.ts +8 -0
- package/dist/extensions/skills/reload.js +38 -0
- package/dist/extensions/skills/reload.js.map +1 -0
- package/dist/extensions/tasks/executor.d.ts +43 -0
- package/dist/extensions/tasks/executor.js +179 -0
- package/dist/extensions/tasks/executor.js.map +1 -0
- package/dist/extensions/tasks/expiration.d.ts +12 -0
- package/dist/extensions/tasks/expiration.js +17 -0
- package/dist/extensions/tasks/expiration.js.map +1 -0
- package/dist/extensions/tasks/generation.d.ts +14 -0
- package/dist/extensions/tasks/generation.js +70 -0
- package/dist/extensions/tasks/generation.js.map +1 -0
- package/dist/extensions/tasks/index.d.ts +14 -0
- package/dist/extensions/tasks/index.js +75 -0
- package/dist/extensions/tasks/index.js.map +1 -0
- package/dist/extensions/tasks/repository.d.ts +53 -0
- package/dist/extensions/tasks/repository.js +147 -0
- package/dist/extensions/tasks/repository.js.map +1 -0
- package/dist/extensions/tasks/schedule.d.ts +13 -0
- package/dist/extensions/tasks/schedule.js +32 -0
- package/dist/extensions/tasks/schedule.js.map +1 -0
- package/dist/extensions/tasks/schema.d.ts +423 -0
- package/dist/extensions/tasks/schema.js +45 -0
- package/dist/extensions/tasks/schema.js.map +1 -0
- package/dist/extensions/tasks/session-delivery.d.ts +18 -0
- package/dist/extensions/tasks/session-delivery.js +39 -0
- package/dist/extensions/tasks/session-delivery.js.map +1 -0
- package/dist/extensions/tasks/tools.d.ts +38 -0
- package/dist/extensions/tasks/tools.js +181 -0
- package/dist/extensions/tasks/tools.js.map +1 -0
- package/dist/extensions/telegram/buttons.d.ts +14 -0
- package/dist/extensions/telegram/buttons.js +49 -0
- package/dist/extensions/telegram/buttons.js.map +1 -0
- package/dist/extensions/telegram/channel.d.ts +39 -0
- package/dist/extensions/telegram/channel.js +201 -0
- package/dist/extensions/telegram/channel.js.map +1 -0
- package/dist/extensions/telegram/chunking.d.ts +7 -0
- package/dist/extensions/telegram/chunking.js +67 -0
- package/dist/extensions/telegram/chunking.js.map +1 -0
- package/dist/extensions/telegram/inbound.d.ts +7 -0
- package/dist/extensions/telegram/inbound.js +29 -0
- package/dist/extensions/telegram/inbound.js.map +1 -0
- package/dist/extensions/telegram/index.d.ts +13 -0
- package/dist/extensions/telegram/index.js +67 -0
- package/dist/extensions/telegram/index.js.map +1 -0
- package/dist/extensions/telegram/media.d.ts +39 -0
- package/dist/extensions/telegram/media.js +223 -0
- package/dist/extensions/telegram/media.js.map +1 -0
- package/dist/extensions/telegram/mutex.d.ts +9 -0
- package/dist/extensions/telegram/mutex.js +14 -0
- package/dist/extensions/telegram/mutex.js.map +1 -0
- package/dist/extensions/telegram/sending.d.ts +48 -0
- package/dist/extensions/telegram/sending.js +119 -0
- package/dist/extensions/telegram/sending.js.map +1 -0
- package/dist/extensions/telegram/streaming.d.ts +46 -0
- package/dist/extensions/telegram/streaming.js +140 -0
- package/dist/extensions/telegram/streaming.js.map +1 -0
- package/dist/extensions/telegram/tools.d.ts +80 -0
- package/dist/extensions/telegram/tools.js +232 -0
- package/dist/extensions/telegram/tools.js.map +1 -0
- package/dist/extensions/workflows/cleanup.d.ts +10 -0
- package/dist/extensions/workflows/cleanup.js +38 -0
- package/dist/extensions/workflows/cleanup.js.map +1 -0
- package/dist/extensions/workflows/index.d.ts +11 -0
- package/dist/extensions/workflows/index.js +42 -0
- package/dist/extensions/workflows/index.js.map +1 -0
- package/dist/extensions/workflows/loader.d.ts +27 -0
- package/dist/extensions/workflows/loader.js +90 -0
- package/dist/extensions/workflows/loader.js.map +1 -0
- package/dist/extensions/workflows/model.d.ts +19 -0
- package/dist/extensions/workflows/model.js +7 -0
- package/dist/extensions/workflows/model.js.map +1 -0
- package/dist/extensions/workflows/repository.d.ts +24 -0
- package/dist/extensions/workflows/repository.js +61 -0
- package/dist/extensions/workflows/repository.js.map +1 -0
- package/dist/extensions/workflows/schema.d.ts +193 -0
- package/dist/extensions/workflows/schema.js +20 -0
- package/dist/extensions/workflows/schema.js.map +1 -0
- package/dist/extensions/workflows/tools.d.ts +27 -0
- package/dist/extensions/workflows/tools.js +285 -0
- package/dist/extensions/workflows/tools.js.map +1 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +15 -0
- package/dist/log.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +27 -0
- package/dist/main.js.map +1 -0
- package/dist/migration/ask.d.ts +8 -0
- package/dist/migration/ask.js +24 -0
- package/dist/migration/ask.js.map +1 -0
- package/dist/migration/config.d.ts +10 -0
- package/dist/migration/config.js +122 -0
- package/dist/migration/config.js.map +1 -0
- package/dist/migration/context.d.ts +3 -0
- package/dist/migration/context.js +24 -0
- package/dist/migration/context.js.map +1 -0
- package/dist/migration/database.d.ts +8 -0
- package/dist/migration/database.js +51 -0
- package/dist/migration/database.js.map +1 -0
- package/dist/migration/fs.d.ts +1 -0
- package/dist/migration/fs.js +11 -0
- package/dist/migration/fs.js.map +1 -0
- package/dist/migration/index.d.ts +11 -0
- package/dist/migration/index.js +28 -0
- package/dist/migration/index.js.map +1 -0
- package/dist/migration/skills.d.ts +19 -0
- package/dist/migration/skills.js +77 -0
- package/dist/migration/skills.js.map +1 -0
- package/dist/scheduler.d.ts +17 -0
- package/dist/scheduler.js +53 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/sessions/registry.d.ts +15 -0
- package/dist/sessions/registry.js +42 -0
- package/dist/sessions/registry.js.map +1 -0
- package/dist/workspace.d.ts +13 -0
- package/dist/workspace.js +32 -0
- package/dist/workspace.js.map +1 -0
- package/drizzle/0000_init.sql +19 -0
- package/drizzle/0001_extensions.sql +63 -0
- package/drizzle/meta/0000_snapshot.json +134 -0
- package/drizzle/meta/0001_snapshot.json +526 -0
- package/drizzle/meta/_journal.json +20 -0
- package/package.json +63 -0
- package/skills/skill-authoring/SKILL.md +168 -0
- package/skills/workflow-authoring/SKILL.md +251 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { defineExtension } from "../api.js";
|
|
4
|
+
const SOUL_TEMPLATE = `# Soul
|
|
5
|
+
|
|
6
|
+
You are Tachikoma, a proactive personal assistant. You maintain persistent memory
|
|
7
|
+
across conversations, learn continuously about the person you assist, and handle
|
|
8
|
+
background work during quiet moments.
|
|
9
|
+
|
|
10
|
+
You are curious, direct, and warm. You take initiative when it helps and stay out
|
|
11
|
+
of the way when it doesn't.
|
|
12
|
+
`;
|
|
13
|
+
const USER_TEMPLATE = `# User
|
|
14
|
+
|
|
15
|
+
Nothing is known about the user yet. This file accumulates durable knowledge about
|
|
16
|
+
who they are, extracted from conversations.
|
|
17
|
+
`;
|
|
18
|
+
const BASE_PROMPT = `You are a personal assistant operating inside your own workspace.
|
|
19
|
+
The workspace is a git-versioned directory that holds your memories, context files, and notes.
|
|
20
|
+
Prefer reading and writing workspace files over guessing; keep your knowledge files current.`;
|
|
21
|
+
const readOrCreate = async (path, template) => {
|
|
22
|
+
try {
|
|
23
|
+
return await readFile(path, "utf8");
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== "ENOENT")
|
|
27
|
+
throw error;
|
|
28
|
+
await writeFile(path, template, "utf8");
|
|
29
|
+
return template;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Foundational context: SOUL.md (personality) and USER.md (durable user knowledge)
|
|
34
|
+
* compose the system prompt. AGENTS.md is discovered natively by pi from the
|
|
35
|
+
* workspace root, so it needs no handling here.
|
|
36
|
+
*/
|
|
37
|
+
export default defineExtension({
|
|
38
|
+
name: "context",
|
|
39
|
+
async setup(app) {
|
|
40
|
+
let soul = "";
|
|
41
|
+
let user = "";
|
|
42
|
+
app.bootstrap("load-context-files", async () => {
|
|
43
|
+
soul = await readOrCreate(app.workspace.resolve("SOUL.md"), SOUL_TEMPLATE);
|
|
44
|
+
user = await readOrCreate(app.workspace.resolve("USER.md"), USER_TEMPLATE);
|
|
45
|
+
});
|
|
46
|
+
// Re-read on every session build so core-context updates from the previous
|
|
47
|
+
// session's post-processing apply without a restart; the bootstrap snapshot
|
|
48
|
+
// is only the fallback when a read fails mid-flight.
|
|
49
|
+
const fresh = (path, fallback) => {
|
|
50
|
+
try {
|
|
51
|
+
return readFileSync(path, "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
app.agent.systemPrompt(() => [
|
|
58
|
+
BASE_PROMPT,
|
|
59
|
+
fresh(app.workspace.resolve("SOUL.md"), soul),
|
|
60
|
+
fresh(app.workspace.resolve("USER.md"), user),
|
|
61
|
+
`Workspace root: ${app.workspace.root}`,
|
|
62
|
+
].join("\n\n"));
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/context/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,aAAa,GAAG;;;;;;;;CAQrB,CAAC;AAEF,MAAM,aAAa,GAAG;;;;CAIrB,CAAC;AAEF,MAAM,WAAW,GAAG;;6FAEyE,CAAC;AAE9F,MAAM,YAAY,GAAG,KAAK,EAAE,IAAY,EAAE,QAAgB,EAAmB,EAAE;IAC7E,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,KAAK,CAAC;QAEpE,MAAM,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QACxC,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC,CAAC;AAEF;;;;GAIG;AACH,eAAe,eAAe,CAAC;IAC7B,IAAI,EAAE,SAAS;IAEf,KAAK,CAAC,KAAK,CAAC,GAAG;QACb,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI,IAAI,GAAG,EAAE,CAAC;QAEd,GAAG,CAAC,SAAS,CAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;YAC7C,IAAI,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC,CAAC;YAC3E,IAAI,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC,CAAC;QAC7E,CAAC,CAAC,CAAC;QAEH,2EAA2E;QAC3E,4EAA4E;QAC5E,qDAAqD;QACrD,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,QAAgB,EAAU,EAAE;YACvD,IAAI,CAAC;gBACH,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,QAAQ,CAAC;YAClB,CAAC;QACH,CAAC,CAAC;QAEF,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,EAAE,CAC1B;YACE,WAAW;YACX,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC;YAC7C,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC;YAC7C,mBAAmB,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE;SACxC,CAAC,IAAI,CAAC,MAAM,CAAC,CACf,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SideRunner } from "../../agent/side-run.ts";
|
|
2
|
+
import type { Logger } from "../../log.ts";
|
|
3
|
+
import type { PostProcessor } from "../api.ts";
|
|
4
|
+
export declare const PENDING_SIGNALS_FILENAME = "pending-signals.md";
|
|
5
|
+
export interface PendingSignal {
|
|
6
|
+
date: string;
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const parsePendingSignals: (content: string) => PendingSignal[];
|
|
10
|
+
/**
|
|
11
|
+
* Drop pending signals older than maxAgeDays. Runs before each context update
|
|
12
|
+
* so the recurrence-detection list never accumulates stale noise.
|
|
13
|
+
*/
|
|
14
|
+
export declare const cleanPendingSignals: (dataDir: string, log: Logger, maxAgeDays?: number) => Promise<void>;
|
|
15
|
+
export interface CoreContextDeps {
|
|
16
|
+
side: Pick<SideRunner, "run">;
|
|
17
|
+
workspaceRoot: string;
|
|
18
|
+
/** Internal data directory holding the pending signals file (never committed). */
|
|
19
|
+
dataDir: string;
|
|
20
|
+
maxTranscriptChars?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Conservative updates to the foundational context files (SOUL.md, USER.md,
|
|
24
|
+
* AGENTS.md at the workspace root) after each session, with a file-based
|
|
25
|
+
* pending-signals list for recurrence detection of ambiguous signals.
|
|
26
|
+
*/
|
|
27
|
+
export declare const createCoreContextProcessor: ({ side, workspaceRoot, dataDir, maxTranscriptChars, }: CoreContextDeps) => PostProcessor;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { localIsoDate } from "../memory/dates.js";
|
|
4
|
+
import { loadConversation } from "../memory/transcript.js";
|
|
5
|
+
export const PENDING_SIGNALS_FILENAME = "pending-signals.md";
|
|
6
|
+
const PENDING_SIGNALS_HEADER = "# Pending Signals\n\n";
|
|
7
|
+
const ENTRY_PATTERN = /^- \*\*(\d{4}-\d{2}-\d{2})\*\*:\s*(.+)$/gm;
|
|
8
|
+
const FILE_TOOLS = ["read", "grep", "find", "ls", "edit", "write"];
|
|
9
|
+
export const parsePendingSignals = (content) => [...content.matchAll(ENTRY_PATTERN)].map((match) => ({
|
|
10
|
+
date: match[1],
|
|
11
|
+
text: match[2],
|
|
12
|
+
}));
|
|
13
|
+
const serializePendingSignals = (entries) => `${PENDING_SIGNALS_HEADER}${entries.map((entry) => `- **${entry.date}**: ${entry.text}`).join("\n")}\n`;
|
|
14
|
+
/**
|
|
15
|
+
* Drop pending signals older than maxAgeDays. Runs before each context update
|
|
16
|
+
* so the recurrence-detection list never accumulates stale noise.
|
|
17
|
+
*/
|
|
18
|
+
export const cleanPendingSignals = async (dataDir, log, maxAgeDays = 30) => {
|
|
19
|
+
const filePath = join(dataDir, PENDING_SIGNALS_FILENAME);
|
|
20
|
+
let content;
|
|
21
|
+
try {
|
|
22
|
+
content = await readFile(filePath, "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (content.trim() === "")
|
|
28
|
+
return;
|
|
29
|
+
const entries = parsePendingSignals(content);
|
|
30
|
+
if (entries.length === 0) {
|
|
31
|
+
log.warn({ file: filePath }, "pending signals file has content but no parseable entries");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const cutoff = new Date();
|
|
35
|
+
cutoff.setDate(cutoff.getDate() - maxAgeDays);
|
|
36
|
+
const cutoffDate = localIsoDate(cutoff);
|
|
37
|
+
// Lexicographic comparison is correct for zero-padded YYYY-MM-DD dates.
|
|
38
|
+
const kept = entries.filter((entry) => entry.date >= cutoffDate);
|
|
39
|
+
if (kept.length === entries.length)
|
|
40
|
+
return;
|
|
41
|
+
if (kept.length === 0) {
|
|
42
|
+
await unlink(filePath);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await writeFile(filePath, serializePendingSignals(kept), "utf8");
|
|
46
|
+
};
|
|
47
|
+
const readPendingSignals = async (filePath) => {
|
|
48
|
+
try {
|
|
49
|
+
return parsePendingSignals(await readFile(filePath, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const formatPendingSignalsSection = (snapshot) => {
|
|
56
|
+
if (snapshot.length === 0)
|
|
57
|
+
return "No pending signals at this time.";
|
|
58
|
+
return snapshot
|
|
59
|
+
.map((entry, index) => `S${index + 1}: **${entry.date}**: ${entry.text}`)
|
|
60
|
+
.join("\n");
|
|
61
|
+
};
|
|
62
|
+
const SYSTEM_TEMPLATE = `You are a context file update agent. Your task is to analyze the completed conversation and update the foundational context files when appropriate.
|
|
63
|
+
|
|
64
|
+
Today's date is {date}.
|
|
65
|
+
|
|
66
|
+
## Your Task
|
|
67
|
+
|
|
68
|
+
1. **Read all three context files:**
|
|
69
|
+
- \`$WORKSPACE/SOUL.md\` — Personality traits, tone, and behavioral guidelines
|
|
70
|
+
- \`$WORKSPACE/USER.md\` — What the assistant knows about the user
|
|
71
|
+
- \`$WORKSPACE/AGENTS.md\` — Operational instructions and workflow preferences
|
|
72
|
+
|
|
73
|
+
2. **Review pending signals:**
|
|
74
|
+
|
|
75
|
+
{pending_signals_section}
|
|
76
|
+
|
|
77
|
+
The pending signals file lives at \`$SIGNALS_FILE\`. Manage it by editing that file directly:
|
|
78
|
+
- To stage a new signal, append a line in the format \`- **{date}**: signal text\`
|
|
79
|
+
- To remove a promoted or stale signal, delete its line
|
|
80
|
+
- Keep the \`# Pending Signals\` header as the first line; create the file with that header if it does not exist yet
|
|
81
|
+
|
|
82
|
+
3. **Analyze the conversation** for information that should update these files:
|
|
83
|
+
|
|
84
|
+
**USER.md** — Stable identity and interests. Things that stay true for weeks or months:
|
|
85
|
+
- Name, location, employer, profession
|
|
86
|
+
- Broad interests and hobbies ("learning trumpet", "game development")
|
|
87
|
+
- Active project NAMES with one-line descriptions — not status, specs, or progress
|
|
88
|
+
- Communication preferences, learning style
|
|
89
|
+
DO NOT put in USER.md: project status updates, detailed specs, meeting prep, daily routine logs, game mechanics, implementation details. If a section is being rewritten more than once a week, it's too detailed for USER.md — that content belongs in memory files (facts or preferences).
|
|
90
|
+
|
|
91
|
+
**SOUL.md** — Personality and behavioral guidelines:
|
|
92
|
+
- Tone and communication style feedback ("be more concise", "push back more")
|
|
93
|
+
- Behavioral instructions that shape the assistant's character
|
|
94
|
+
|
|
95
|
+
**AGENTS.md** — Operational instructions and workflow preferences:
|
|
96
|
+
- Tool usage patterns, CLI preferences
|
|
97
|
+
- Workflow conventions, formatting rules
|
|
98
|
+
- System-specific instructions (task scheduling, note creation patterns)
|
|
99
|
+
|
|
100
|
+
**Correction Detection** — Watch for moments where the agent was corrected and extract the lesson as a behavioral instruction:
|
|
101
|
+
- **Explicit user corrections**: The user directly says "no", "don't", "wrong", "actually", or otherwise rejects the agent's approach and provides the right one
|
|
102
|
+
- **Implicit user corrections**: The user restates or rephrases their request after the agent gave a clearly wrong answer, or provides the correct answer themselves after the agent was wrong — only when the agent demonstrably erred, not normal conversational refinement
|
|
103
|
+
- **Agent self-corrections**: The agent acknowledges a mistake ("I was wrong", "let me fix that") and provides the corrected approach
|
|
104
|
+
|
|
105
|
+
When a correction is detected:
|
|
106
|
+
- Extract the lesson as a concise, positive instruction that describes the correct behavior: \`- When [context], [correct behavior].\`
|
|
107
|
+
- Lead with what to do, not what went wrong. The entry should teach the right approach as if explaining to a colleague — natural, direct, and actionable
|
|
108
|
+
- Place the entry under the AGENTS.md section that matches its domain. This keeps related instructions together. If no matching section exists, create one with a descriptive heading.
|
|
109
|
+
- Before adding, read existing entries in that section and skip if a semantically similar entry already covers it — or refine the existing entry if the correction adds new nuance (e.g., a missing condition or clarified boundary)
|
|
110
|
+
- Keep entries to one line each. No explanations, no context, no history
|
|
111
|
+
|
|
112
|
+
**Routing note**: Corrections about task execution, tool usage, or problem-solving go to AGENTS.md under the domain-appropriate section. Corrections about communication style or tone (e.g., "be more casual") go to SOUL.md as personality adjustments — those are not corrections.
|
|
113
|
+
|
|
114
|
+
4. **Classify each signal** and take action:
|
|
115
|
+
|
|
116
|
+
**Clear & explicit signals** (strong evidence, unambiguous):
|
|
117
|
+
- Update the appropriate context file directly
|
|
118
|
+
- Read the file first, preserve structure, merge changes contextually
|
|
119
|
+
- Replace outdated information when there's clear evidence of change
|
|
120
|
+
|
|
121
|
+
**Ambiguous / one-off signals** (single mention, no clear directive):
|
|
122
|
+
- Check the pending signals list above for semantic recurrence
|
|
123
|
+
- If a recurring pattern is detected → promote it to a context file update AND delete the promoted signal's line from the pending signals file
|
|
124
|
+
- If it's a first occurrence → stage it as a new pending signal for future tracking
|
|
125
|
+
|
|
126
|
+
**Stale or irrelevant signals in the list:**
|
|
127
|
+
- Delete their lines to prevent noise in future sessions
|
|
128
|
+
|
|
129
|
+
**No relevant information** → do nothing (this is perfectly acceptable)
|
|
130
|
+
|
|
131
|
+
5. **Prune stale content** from context files:
|
|
132
|
+
- While reading context files, actively look for content that is outdated or no longer accurate:
|
|
133
|
+
- USER.md: projects that were completed or abandoned (confirmed by conversation), outdated employer or role info, interests the user has moved away from, resolved bugs or issues the user discussed in the past, completed one-time tasks or work items, past events or completed trips, one-time plans that are now past
|
|
134
|
+
- AGENTS.md: entries about resolved bugs or completed work (the fix is done, the instruction is no longer needed), entries that duplicate another section (keep the better version and remove the other), procedural step-by-step instructions that belong elsewhere, outdated conventions
|
|
135
|
+
- SOUL.md: personality adjustments that the user has contradicted or reversed
|
|
136
|
+
- **Consolidate duplicate sections**: If two sections in the same file cover the same topic with semantically equivalent content, merge them into one section combining the best of both. Only consolidate when sections are truly equivalent — related-but-distinct topics (e.g., "remote work preferences" vs "home office equipment") must remain separate.
|
|
137
|
+
- Remove or update stale sections to keep files current and concise. Do not leave outdated info "just in case" — these files should be a current snapshot, not an archive.
|
|
138
|
+
- **Do NOT prune based on**: vague hints, assumptions, or the age of content alone (age is not staleness — only prune when you have clear evidence)
|
|
139
|
+
|
|
140
|
+
6. **Important constraints:**
|
|
141
|
+
- **Be conservative**: Only apply changes with clear conversational evidence
|
|
142
|
+
- **Route correctly**: personality→SOUL, user info→USER, instructions→AGENTS
|
|
143
|
+
- **Read-first**: Always read a file before modifying it
|
|
144
|
+
- **Preserve structure**: Keep existing formatting and organization
|
|
145
|
+
- **Watch file size**: USER.md should stay under ~120 lines, AGENTS.md under ~400 lines. When a file exceeds its limit, prune actively:
|
|
146
|
+
- USER.md: summarize, remove stale sections, or omit details that belong in facts/preferences memory
|
|
147
|
+
- AGENTS.md: remove entries about resolved bugs or completed work, and consolidate duplicated entries across sections
|
|
148
|
+
- **Replace, don't append**: When updating a section, rewrite it cleanly rather than appending new paragraphs. Each section should read as a current snapshot, not a changelog.
|
|
149
|
+
|
|
150
|
+
## Pending Signals Lifecycle
|
|
151
|
+
|
|
152
|
+
The pending signals mechanism tracks ambiguous observations that might become patterns if they recur:
|
|
153
|
+
|
|
154
|
+
1. **Stage**: When you notice a potential signal but it's ambiguous or one-off, append a dated line to the pending signals file.
|
|
155
|
+
|
|
156
|
+
2. **Promote**: When you detect a recurring pattern in pending signals, update the appropriate context file AND delete the promoted lines from the pending signals file.
|
|
157
|
+
|
|
158
|
+
3. **Cleanup**: When you notice stale or irrelevant signals in the list, delete their lines proactively rather than waiting for the 30-day expiry.
|
|
159
|
+
|
|
160
|
+
## Examples
|
|
161
|
+
|
|
162
|
+
### Clear Signal → Direct Update
|
|
163
|
+
User: "I just started a new job at Acme Corp"
|
|
164
|
+
Action: Update USER.md with new employer information
|
|
165
|
+
|
|
166
|
+
### Ambiguous Signal → Stage
|
|
167
|
+
User: "that was too verbose"
|
|
168
|
+
Action: Check pending signals above. If no similar signal, append a dated entry to the pending signals file for recurrence detection.
|
|
169
|
+
|
|
170
|
+
### Recurring Signal → Promote and Remove
|
|
171
|
+
Pending signals: S1: "User seemed to prefer shorter responses"
|
|
172
|
+
Current message: "your answers are way too long"
|
|
173
|
+
Action: This confirms a pattern → update SOUL.md with preference for concise responses, then delete the S1 line from the pending signals file.
|
|
174
|
+
|
|
175
|
+
### Stale Signal → Cleanup
|
|
176
|
+
Pending signals: S2: "User mentioned liking dark themes" (from 3 weeks ago, no recurrence in subsequent conversations)
|
|
177
|
+
Action: Delete the S2 line from the pending signals file.
|
|
178
|
+
|
|
179
|
+
### Stale Content → Prune
|
|
180
|
+
USER.md contains: "- Planning trip to Berlin (March 15-20)"
|
|
181
|
+
Conversation reveals: The trip happened and is now in the past.
|
|
182
|
+
Action: Remove the trip entry — it's time-specific and no longer current.
|
|
183
|
+
|
|
184
|
+
### Duplicate Sections → Consolidate
|
|
185
|
+
AGENTS.md has both a "Code Review" section and a "PR Conventions" section covering the same review workflow rules.
|
|
186
|
+
Action: Merge into a single "Code Review" section combining the rules from both.
|
|
187
|
+
|
|
188
|
+
## Workspace Validation
|
|
189
|
+
|
|
190
|
+
Before writing claims about workspace state — file paths, project structure, configuration values — verify each claim directly by reading the relevant file(s) or grepping for the referenced content. Omit claims you cannot verify. Do NOT validate subjective information, preferences, or personal details.
|
|
191
|
+
|
|
192
|
+
## Scope
|
|
193
|
+
|
|
194
|
+
You can read files anywhere in the workspace (needed for validation). Only modify \`$WORKSPACE/SOUL.md\`, \`$WORKSPACE/USER.md\`, \`$WORKSPACE/AGENTS.md\`, and the pending signals file at \`$SIGNALS_FILE\`.
|
|
195
|
+
|
|
196
|
+
## Remember
|
|
197
|
+
|
|
198
|
+
These files shape the assistant's identity and behavior across all sessions. Updates should be deliberate and evidence-based. When in doubt, stage the signal for future recurrence detection rather than making premature changes.`;
|
|
199
|
+
/**
|
|
200
|
+
* Conservative updates to the foundational context files (SOUL.md, USER.md,
|
|
201
|
+
* AGENTS.md at the workspace root) after each session, with a file-based
|
|
202
|
+
* pending-signals list for recurrence detection of ambiguous signals.
|
|
203
|
+
*/
|
|
204
|
+
export const createCoreContextProcessor = ({ side, workspaceRoot, dataDir, maxTranscriptChars = 24000, }) => ({
|
|
205
|
+
name: "core-context",
|
|
206
|
+
phase: "preFinalize",
|
|
207
|
+
async process({ transcriptPath, log }) {
|
|
208
|
+
if (transcriptPath == null) {
|
|
209
|
+
log.debug("no transcript — skipping core context update");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const conversation = await loadConversation(transcriptPath, maxTranscriptChars);
|
|
213
|
+
if (conversation === "") {
|
|
214
|
+
log.debug("empty conversation — skipping core context update");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await cleanPendingSignals(dataDir, log);
|
|
218
|
+
const signalsFile = join(dataDir, PENDING_SIGNALS_FILENAME);
|
|
219
|
+
const snapshot = await readPendingSignals(signalsFile);
|
|
220
|
+
const system = SYSTEM_TEMPLATE.replaceAll("{pending_signals_section}", formatPendingSignalsSection(snapshot))
|
|
221
|
+
.replaceAll("{date}", localIsoDate())
|
|
222
|
+
.replaceAll("$WORKSPACE", workspaceRoot)
|
|
223
|
+
.replaceAll("$SIGNALS_FILE", signalsFile);
|
|
224
|
+
const prompt = `The following conversation with the user just ended:\n\n<conversation>\n${conversation}\n</conversation>\n\nFollow your instructions and update the context files accordingly.`;
|
|
225
|
+
await side.run({ tools: FILE_TOOLS, system, prompt, tier: "processor" });
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
//# sourceMappingURL=processor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processor.js","sourceRoot":"","sources":["../../../src/extensions/context/processor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface DetachedProcessesConfig {
|
|
2
|
+
/** Default memory limit applied to spawned processes; 0 disables the default. */
|
|
3
|
+
defaultMemoryLimitMb: number;
|
|
4
|
+
watchIntervalSeconds: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Detached processes: spawn shell commands that outlive Tachikoma, capture
|
|
8
|
+
* their output to files, watch for exits, and notify on completion. The agent
|
|
9
|
+
* manages them through the process tools.
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: import("../api.ts").TachikomaExtension<DetachedProcessesConfig>;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { defineExtension } from "../api.js";
|
|
5
|
+
import { SystemdRunLimiter } from "./limits.js";
|
|
6
|
+
import { reconcileOnStartup } from "./reconcile.js";
|
|
7
|
+
import { ProcessRepository } from "./repository.js";
|
|
8
|
+
import { createProcessToolsFactory } from "./tools.js";
|
|
9
|
+
import { createWatcherTick } from "./watcher.js";
|
|
10
|
+
/**
|
|
11
|
+
* Detached processes: spawn shell commands that outlive Tachikoma, capture
|
|
12
|
+
* their output to files, watch for exits, and notify on completion. The agent
|
|
13
|
+
* manages them through the process tools.
|
|
14
|
+
*/
|
|
15
|
+
export default defineExtension({
|
|
16
|
+
name: "detached-processes",
|
|
17
|
+
configSchema: Type.Object({
|
|
18
|
+
defaultMemoryLimitMb: Type.Number({ default: 1024 }),
|
|
19
|
+
watchIntervalSeconds: Type.Number({ default: 15 }),
|
|
20
|
+
}),
|
|
21
|
+
setup(app) {
|
|
22
|
+
const repository = new ProcessRepository(app.db);
|
|
23
|
+
const limiter = new SystemdRunLimiter(app.log);
|
|
24
|
+
const processesDir = join(app.workspace.dataDir, "processes");
|
|
25
|
+
const reconcile = {
|
|
26
|
+
repository,
|
|
27
|
+
processesDir,
|
|
28
|
+
notify: (notification) => app.events.emit("notify", {
|
|
29
|
+
title: `Process ${notification.processId}`,
|
|
30
|
+
text: notification.message,
|
|
31
|
+
severity: notification.severity === "error" ? "warning" : "info",
|
|
32
|
+
source: notification.source,
|
|
33
|
+
}),
|
|
34
|
+
log: app.log,
|
|
35
|
+
};
|
|
36
|
+
app.bootstrap("reconcile", async () => {
|
|
37
|
+
await mkdir(processesDir, { recursive: true });
|
|
38
|
+
await limiter.detect();
|
|
39
|
+
await reconcileOnStartup(reconcile);
|
|
40
|
+
});
|
|
41
|
+
app.agent.use(createProcessToolsFactory({
|
|
42
|
+
...reconcile,
|
|
43
|
+
limiter,
|
|
44
|
+
defaultMemoryLimitMb: app.extensionConfig.defaultMemoryLimitMb > 0
|
|
45
|
+
? app.extensionConfig.defaultMemoryLimitMb
|
|
46
|
+
: null,
|
|
47
|
+
}));
|
|
48
|
+
app.scheduler.every("detached-watch", app.extensionConfig.watchIntervalSeconds, createWatcherTick(reconcile));
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/detached-processes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/B,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAgD,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAClG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAQjD;;;;GAIG;AACH,eAAe,eAAe,CAA0B;IACtD,IAAI,EAAE,oBAAoB;IAE1B,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC;QACxB,oBAAoB,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpD,oBAAoB,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;KACnD,CAAC;IAEF,KAAK,CAAC,GAAG;QACP,MAAM,UAAU,GAAG,IAAI,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/C,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QAE9D,MAAM,SAAS,GAAkB;YAC/B,UAAU;YACV,YAAY;YACZ,MAAM,EAAE,CAAC,YAAiC,EAAE,EAAE,CAC5C,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE;gBACxB,KAAK,EAAE,WAAW,YAAY,CAAC,SAAS,EAAE;gBAC1C,IAAI,EAAE,YAAY,CAAC,OAAO;gBAC1B,QAAQ,EAAE,YAAY,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;gBAChE,MAAM,EAAE,YAAY,CAAC,MAAM;aAC5B,CAAC;YACJ,GAAG,EAAE,GAAG,CAAC,GAAG;SACb,CAAC;QAEF,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE;YACpC,MAAM,KAAK,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC;YACvB,MAAM,kBAAkB,CAAC,SAAS,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,KAAK,CAAC,GAAG,CACX,yBAAyB,CAAC;YACxB,GAAG,SAAS;YACZ,OAAO;YACP,oBAAoB,EAClB,GAAG,CAAC,eAAe,CAAC,oBAAoB,GAAG,CAAC;gBAC1C,CAAC,CAAC,GAAG,CAAC,eAAe,CAAC,oBAAoB;gBAC1C,CAAC,CAAC,IAAI;SACX,CAAC,CACH,CAAC;QAEF,GAAG,CAAC,SAAS,CAAC,KAAK,CACjB,gBAAgB,EAChB,GAAG,CAAC,eAAe,CAAC,oBAAoB,EACxC,iBAAiB,CAAC,SAAS,CAAC,CAC7B,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Logger } from "../../log.ts";
|
|
2
|
+
export interface WrappedCommand {
|
|
3
|
+
file: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
/** Whether a memory limit is actually enforced on the spawned process. */
|
|
6
|
+
limited: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Seam for resource limiting; a direct cgroup v2 implementation can slot in
|
|
10
|
+
* behind this interface.
|
|
11
|
+
*/
|
|
12
|
+
export interface ProcessLimiter {
|
|
13
|
+
wrap(command: string, memoryLimitMb: number | null): WrappedCommand;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Limits via `systemd-run --user --scope -p MemoryMax=…`: systemd places the
|
|
17
|
+
* command in a transient scope (cgroup) and exits with the command's status,
|
|
18
|
+
* so liveness checks and exit codes behave like an unwrapped spawn.
|
|
19
|
+
*/
|
|
20
|
+
export declare class SystemdRunLimiter implements ProcessLimiter {
|
|
21
|
+
private available;
|
|
22
|
+
private readonly log;
|
|
23
|
+
constructor(log: Logger);
|
|
24
|
+
/** Probe `systemd-run` once at bootstrap; wrap() degrades gracefully without it. */
|
|
25
|
+
detect(probe?: () => Promise<unknown>): Promise<void>;
|
|
26
|
+
wrap(command: string, memoryLimitMb: number | null): WrappedCommand;
|
|
27
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const plainShell = (command) => ({
|
|
5
|
+
file: "sh",
|
|
6
|
+
args: ["-c", command],
|
|
7
|
+
limited: false,
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Limits via `systemd-run --user --scope -p MemoryMax=…`: systemd places the
|
|
11
|
+
* command in a transient scope (cgroup) and exits with the command's status,
|
|
12
|
+
* so liveness checks and exit codes behave like an unwrapped spawn.
|
|
13
|
+
*/
|
|
14
|
+
export class SystemdRunLimiter {
|
|
15
|
+
available = false;
|
|
16
|
+
log;
|
|
17
|
+
constructor(log) {
|
|
18
|
+
this.log = log;
|
|
19
|
+
}
|
|
20
|
+
/** Probe `systemd-run` once at bootstrap; wrap() degrades gracefully without it. */
|
|
21
|
+
async detect(probe = () => execFileAsync("systemd-run", ["--version"])) {
|
|
22
|
+
try {
|
|
23
|
+
await probe();
|
|
24
|
+
this.available = true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
this.available = false;
|
|
28
|
+
this.log.info("systemd-run not available — processes will run without memory limits");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
wrap(command, memoryLimitMb) {
|
|
32
|
+
if (memoryLimitMb == null)
|
|
33
|
+
return plainShell(command);
|
|
34
|
+
if (!this.available) {
|
|
35
|
+
this.log.warn({ memoryLimitMb }, "systemd-run unavailable — spawning without memory limit");
|
|
36
|
+
return plainShell(command);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
file: "systemd-run",
|
|
40
|
+
args: [
|
|
41
|
+
"--user",
|
|
42
|
+
"--scope",
|
|
43
|
+
"--quiet",
|
|
44
|
+
"-p",
|
|
45
|
+
`MemoryMax=${memoryLimitMb}M`,
|
|
46
|
+
"--",
|
|
47
|
+
"sh",
|
|
48
|
+
"-c",
|
|
49
|
+
command,
|
|
50
|
+
],
|
|
51
|
+
limited: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../../src/extensions/detached-processes/limits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAItC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAiB1C,MAAM,UAAU,GAAG,CAAC,OAAe,EAAkB,EAAE,CAAC,CAAC;IACvD,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,KAAK;CACf,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,OAAO,iBAAiB;IACpB,SAAS,GAAG,KAAK,CAAC;IACT,GAAG,CAAS;IAE7B,YAAY,GAAW;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED,oFAAoF;IACpF,KAAK,CAAC,MAAM,CACV,QAAgC,GAAG,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,CAAC,WAAW,CAAC,CAAC;QAEjF,IAAI,CAAC;YACH,MAAM,KAAK,EAAE,CAAC;YACd,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,aAA4B;QAChD,IAAI,aAAa,IAAI,IAAI;YAAE,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;QAEtD,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,EAAE,yDAAyD,CAAC,CAAC;YAC5F,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;QAED,OAAO;YACL,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE;gBACJ,QAAQ;gBACR,SAAS;gBACT,SAAS;gBACT,IAAI;gBACJ,aAAa,aAAa,GAAG;gBAC7B,IAAI;gBACJ,IAAI;gBACJ,IAAI;gBACJ,OAAO;aACR;YACD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { open, stat } from "node:fs/promises";
|
|
2
|
+
// Generous raw window — truncateTail trims it to pi's byte/line limits afterwards.
|
|
3
|
+
const TAIL_READ_BYTES = 256 * 1024;
|
|
4
|
+
/** Read the last chunk of a log file; null when the file does not exist. */
|
|
5
|
+
export const readOutputTail = async (path) => {
|
|
6
|
+
let size;
|
|
7
|
+
try {
|
|
8
|
+
size = (await stat(path)).size;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (size === 0)
|
|
14
|
+
return "";
|
|
15
|
+
const start = Math.max(0, size - TAIL_READ_BYTES);
|
|
16
|
+
const handle = await open(path, "r");
|
|
17
|
+
try {
|
|
18
|
+
const buffer = Buffer.alloc(size - start);
|
|
19
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
|
|
20
|
+
return buffer.subarray(0, bytesRead).toString("utf-8");
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await handle.close();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../../src/extensions/detached-processes/output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE9C,mFAAmF;AACnF,MAAM,eAAe,GAAG,GAAG,GAAG,IAAI,CAAC;AAEnC,4EAA4E;AAC5E,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,IAAY,EAA0B,EAAE;IAC3E,IAAI,IAAY,CAAC;IAEjB,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAE1B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,eAAe,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAErC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;QAC1C,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAEzE,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACzD,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Logger } from "../../log.ts";
|
|
2
|
+
import type { ProcessRepository } from "./repository.ts";
|
|
3
|
+
export interface ProcessNotification {
|
|
4
|
+
source: string;
|
|
5
|
+
processId: string;
|
|
6
|
+
severity: "info" | "error";
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ReconcileDeps {
|
|
10
|
+
repository: ProcessRepository;
|
|
11
|
+
processesDir: string;
|
|
12
|
+
notify: (notification: ProcessNotification) => void;
|
|
13
|
+
log: Logger;
|
|
14
|
+
now?: () => Date;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Transition a running record to exited, optionally dispatching a notification.
|
|
18
|
+
* Idempotent: a conditional UPDATE makes concurrent reconcilers converge to a
|
|
19
|
+
* single winner, and only the winner notifies.
|
|
20
|
+
*/
|
|
21
|
+
export declare const reconcileExit: (deps: ReconcileDeps, recordId: string, { dispatchNotification }?: {
|
|
22
|
+
dispatchNotification?: boolean;
|
|
23
|
+
}) => Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Crash recovery: reconcile records whose processes died while the host was
|
|
26
|
+
* down. Notifications are suppressed so the user doesn't get a burst on
|
|
27
|
+
* restart. Records whose pids are still alive simply stay running — without a
|
|
28
|
+
* create-time check a reused pid would keep a record alive until its next
|
|
29
|
+
* natural exit, which the polling watcher then reconciles.
|
|
30
|
+
*/
|
|
31
|
+
export declare const reconcileOnStartup: (deps: ReconcileDeps) => Promise<void>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
|
+
import { STOP_REASON_AGENT_STOPPED } from "./schema.js";
|
|
4
|
+
import { exitCodePath, isAlive } from "./spawn.js";
|
|
5
|
+
const SIGKILL_EXIT_CODE = 137;
|
|
6
|
+
const readExitCode = async (path) => {
|
|
7
|
+
// Retry once after 100ms: the watcher's kill(pid, 0) can observe death before
|
|
8
|
+
// the in-process exit listener has written the sidecar.
|
|
9
|
+
for (const delayMs of [0, 100]) {
|
|
10
|
+
if (delayMs > 0)
|
|
11
|
+
await sleep(delayMs);
|
|
12
|
+
try {
|
|
13
|
+
const parsed = Number.parseInt(await readFile(path, "utf-8"), 10);
|
|
14
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Missing sidecar — exit happened while the host was down (unknowable code).
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Transition a running record to exited, optionally dispatching a notification.
|
|
24
|
+
* Idempotent: a conditional UPDATE makes concurrent reconcilers converge to a
|
|
25
|
+
* single winner, and only the winner notifies.
|
|
26
|
+
*/
|
|
27
|
+
export const reconcileExit = async (deps, recordId, { dispatchNotification = true } = {}) => {
|
|
28
|
+
const { repository, processesDir, notify, log } = deps;
|
|
29
|
+
const now = deps.now ?? (() => new Date());
|
|
30
|
+
try {
|
|
31
|
+
const record = repository.get(recordId);
|
|
32
|
+
if (record == null || record.status !== "running")
|
|
33
|
+
return;
|
|
34
|
+
const exitCode = await readExitCode(exitCodePath(processesDir, record.id));
|
|
35
|
+
const won = repository.reconcileToExited(record.id, now(), exitCode);
|
|
36
|
+
if (!won || !dispatchNotification)
|
|
37
|
+
return;
|
|
38
|
+
if (record.stopReason === STOP_REASON_AGENT_STOPPED) {
|
|
39
|
+
log.debug({ id: recordId }, "suppressing exit notification for agent-stopped process");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const message = exitCode === SIGKILL_EXIT_CODE
|
|
43
|
+
? `Process '${record.name}' (id: ${record.id}) was killed by signal (SIGKILL).`
|
|
44
|
+
: `Process '${record.name}' (id: ${record.id}) exited with code ${exitCode ?? "unknown"}.`;
|
|
45
|
+
notify({
|
|
46
|
+
source: `Detached process: ${record.name}`,
|
|
47
|
+
processId: record.id,
|
|
48
|
+
severity: exitCode === 0 ? "info" : "error",
|
|
49
|
+
message,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
log.error({ id: recordId, err: error }, "error reconciling process");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Crash recovery: reconcile records whose processes died while the host was
|
|
58
|
+
* down. Notifications are suppressed so the user doesn't get a burst on
|
|
59
|
+
* restart. Records whose pids are still alive simply stay running — without a
|
|
60
|
+
* create-time check a reused pid would keep a record alive until its next
|
|
61
|
+
* natural exit, which the polling watcher then reconciles.
|
|
62
|
+
*/
|
|
63
|
+
export const reconcileOnStartup = async (deps) => {
|
|
64
|
+
for (const record of deps.repository.listRunning()) {
|
|
65
|
+
if (isAlive(record.pid))
|
|
66
|
+
continue;
|
|
67
|
+
await reconcileExit(deps, record.id, { dispatchNotification: false });
|
|
68
|
+
deps.log.info({ id: record.id }, "crash recovery: marked process as exited");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../../src/extensions/detached-processes/reconcile.ts"],"names":[],"mappings":"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"}
|