agentbox-app 0.1.7 → 0.1.12
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 +220 -58
- package/bin/agentbox.mjs +18 -3
- package/briefs/message-rules.md +24 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +11 -11
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +116 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/approvals.mjs +14 -5
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +32 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +25 -13
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +18 -0
- package/main/codex-launch-env.mjs +31 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +82 -17
- package/main/demo.mjs +17 -14
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +204 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +7 -4
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +9 -3
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +134 -60
- package/main/ipc.mjs +335 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +48 -1
- package/main/main.mjs +138 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/notify.mjs +1 -1
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +21 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +115 -51
- package/main/runs-on.mjs +93 -0
- package/main/serve.mjs +77 -5
- package/main/settings.mjs +245 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +84 -7
- package/main/store/project.mjs +15 -19
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +245 -13
- package/main/supervisor.mjs +1634 -417
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +94 -16
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +14 -2
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +10 -12
- package/mcp/core/work.mjs +123 -26
- package/mcp/tools.mjs +13 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
- package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/after-install.mjs +14 -0
- package/scripts/approval-server.sh +7 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +39 -16
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +38 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +6 -6
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-modes.mjs +1 -1
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +6 -6
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +34 -4
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +9 -3
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/runs-on.d.mts +20 -0
- package/shared/runs-on.mjs +79 -0
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +40 -19
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +258 -28
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- package/renderer/dist/emulator/provenance.json +0 -6
package/shared/work-items.mjs
CHANGED
|
@@ -66,7 +66,7 @@ export const CLAUDE_PERMISSION_MODES = [
|
|
|
66
66
|
// `answerMode` used to live on the ledger: what one reply is allowed to do on
|
|
67
67
|
// the founder's Mac, turned straight into `--permission-mode` on a real process
|
|
68
68
|
// by spawnPlan. A guard here stripped it off any line that did not claim to be
|
|
69
|
-
//
|
|
69
|
+
// the founder's, which closed the STRUCTURED path, the MCP and the app.
|
|
70
70
|
//
|
|
71
71
|
// IT COULD NOT CLOSE THE FILE, AND NOTHING WRITTEN HERE EVER COULD. `source` is
|
|
72
72
|
// a plain string in a line and nothing checks it. A worker with Bash could
|
|
@@ -78,8 +78,8 @@ export const CLAUDE_PERMISSION_MODES = [
|
|
|
78
78
|
// own storage now, written by main and by nothing else (main/answer-modes.mjs),
|
|
79
79
|
// which is the convention Claude Code uses for its standing policy. Everything
|
|
80
80
|
// on the ledger today is a claim about the WORK, and a forged line can still
|
|
81
|
-
// fake
|
|
82
|
-
// filed
|
|
81
|
+
// fake the founder's archive or answer, which is true of any append-only log
|
|
82
|
+
// nobody signs, and is filed as a known issue rather than papered over here. What it can no longer do is hand
|
|
83
83
|
// anything a permission.
|
|
84
84
|
//
|
|
85
85
|
// If a future field is a grant rather than an opinion, the answer is the same
|
|
@@ -90,29 +90,96 @@ export const CLAUDE_PERMISSION_MODES = [
|
|
|
90
90
|
// state is worse than one that visibly does nothing.
|
|
91
91
|
// `label` IS THE NAME ON THE ROW, AND IT EXISTS BECAUSE `title` CANNOT BE ONE.
|
|
92
92
|
//
|
|
93
|
-
// A message
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// "We". Eleven of those in a column is eleven rows that look like one row.
|
|
93
|
+
// A dictated message becomes an item whose title is its first line, verbatim
|
|
94
|
+
// (message-split.ts). Such titles run long, are often cut off mid sentence with
|
|
95
|
+
// an ellipsis, and many open with "I", "I've been", "Can you" or "We". Eleven
|
|
96
|
+
// of those in a column is eleven rows that look like one row.
|
|
98
97
|
//
|
|
99
98
|
// The obvious fix, having a session write a better title, does not work and
|
|
100
|
-
// never did: `beats` below makes
|
|
101
|
-
//
|
|
102
|
-
// dropped on the floor, silently. That is the whole
|
|
103
|
-
// field rather than a better-behaved writer.
|
|
99
|
+
// never did: `beats` below makes the founder outrank an agent per field, so
|
|
100
|
+
// nearly every such title is the founder's and an agent's rewrite of one is
|
|
101
|
+
// accepted here and then dropped on the floor, silently. That is the whole
|
|
102
|
+
// reason this is a second field rather than a better-behaved writer.
|
|
104
103
|
//
|
|
105
|
-
// So `title` stays exactly what
|
|
106
|
-
// `label` is the short written name a session puts beside it, and the
|
|
107
|
-
// prefers it (`rowTitle`, list-rules.ts). Everything else still reads the
|
|
108
|
-
// so
|
|
109
|
-
// either name.
|
|
104
|
+
// So `title` stays exactly what the user typed and nothing about who owns it
|
|
105
|
+
// changes. `label` is the short written name a session puts beside it, and the
|
|
106
|
+
// LIST prefers it (`rowTitle`, list-rules.ts). Everything else still reads the
|
|
107
|
+
// title, so the user's own words are what they find when they open the row, and
|
|
108
|
+
// search matches either name.
|
|
110
109
|
export const WORK_ITEM_FIELDS = [
|
|
111
110
|
'title', 'label', 'body', 'kind', 'labels', 'priority', 'parent',
|
|
112
|
-
'status', 'result', 'note', 'answer', 'product', 'runAt', 'answeredThrough',
|
|
111
|
+
'status', 'result', 'note', 'answer', 'product', 'runAt', 'snoozedUntil', 'answeredThrough',
|
|
113
112
|
'engine', 'model', 'effort',
|
|
113
|
+
// WHETHER THIS HAS BEEN STARTED AT ALL. 'later' is a thread written down and
|
|
114
|
+
// deliberately not begun: it waits for a person, not for a clock, and
|
|
115
|
+
// `isDue` below is where that becomes true of the whole app. 'now' is how it
|
|
116
|
+
// is started, and a thread without the field at all is an ordinary one that
|
|
117
|
+
// started the moment it was sent.
|
|
118
|
+
'start',
|
|
119
|
+
// THE TEAM FIELDS. All four are about PEOPLE on a shared project, and a
|
|
120
|
+
// private project never sets them, so a row without them reads as before.
|
|
121
|
+
// assignee the person who has to act next, or 'agent' once a person has
|
|
122
|
+
// handed it back to the agents
|
|
123
|
+
// runner the person whose Mac runs agents on this row; without one it
|
|
124
|
+
// is whoever started it (`createdBy`)
|
|
125
|
+
// due a calendar day, YYYY-MM-DD, or 'none' to clear one
|
|
126
|
+
// people who is on the conversation
|
|
127
|
+
'assignee', 'runner', 'due', 'people',
|
|
128
|
+
// THE THREAD'S OWN FIELDS (the team version, approved 2026-10-01).
|
|
129
|
+
// visibility 'team' (the default), 'people' or 'private': whether every
|
|
130
|
+
// teammate sees this thread's summary on the Team board, only
|
|
131
|
+
// the people named in `visibleTo`, or nobody but you
|
|
132
|
+
// visibleTo the people a 'people' thread reaches, by id. Ignored on the
|
|
133
|
+
// other two, and a 'people' thread naming nobody reaches
|
|
134
|
+
// nobody (shared/thread-cards.mjs says why it fails closed)
|
|
135
|
+
// problem, progress, solution
|
|
136
|
+
// the SUMMARY, one short sentence each (an agent's is held to
|
|
137
|
+
// SUMMARY_WORDS, shared/thread-cards.mjs). The agent keeps it
|
|
138
|
+
// current and the person can edit it in place, so these are
|
|
139
|
+
// the fields where the later write wins whoever wrote it
|
|
140
|
+
// (SHARED_FIELDS below)
|
|
141
|
+
// blockedBy, blocks
|
|
142
|
+
// other threads' ids, the summary's Linked part
|
|
143
|
+
'visibility', 'visibleTo', 'problem', 'progress', 'solution', 'blockedBy', 'blocks',
|
|
144
|
+
// A REACTION, AND IT IS THE ONE FIELD ON THIS LIST THAT IS A DELTA RATHER
|
|
145
|
+
// THAN A VALUE: `{ on, emoji }` to put one on, `{ on, emoji, off: true }` to
|
|
146
|
+
// take yours back, where `on` is the uid of the line the message was written
|
|
147
|
+
// as. Every other field here keeps the latest write and drops the rest, which
|
|
148
|
+
// for a reaction would mean your 👀 erasing the one a teammate put on the same
|
|
149
|
+
// message a second earlier. The fold accumulates these per person instead and
|
|
150
|
+
// reads them back as `reactions` (see foldWorkItems); nothing ever reads a
|
|
151
|
+
// bare `react` off an item.
|
|
152
|
+
'react',
|
|
153
|
+
// A REPLY IN A THREAD (w-920461cbe6): the uid of the message line it answers,
|
|
154
|
+
// written on the same line as the reply's own `answer`. Like `react` it is a
|
|
155
|
+
// mark on ONE line rather than a value the row holds, so the fold skips it;
|
|
156
|
+
// the chat reads it per line (renderer/src/thread-history.ts).
|
|
157
|
+
'inReplyTo',
|
|
114
158
|
];
|
|
115
159
|
|
|
160
|
+
// THE SUMMARY IS SHARED BETWEEN THE PERSON AND THE AGENT. Everywhere else the
|
|
161
|
+
// founder's word outranks an agent's forever, which here would mean one edit
|
|
162
|
+
// of hers freezes the summary the agent is meant to keep up to date. So on
|
|
163
|
+
// these fields the later write wins, whoever made it, and the source is still
|
|
164
|
+
// recorded so the panel can say who touched it last.
|
|
165
|
+
const SHARED_FIELDS = new Set(['problem', 'progress', 'solution', 'blockedBy', 'blocks']);
|
|
166
|
+
const MAX_SUMMARY = 1200;
|
|
167
|
+
|
|
168
|
+
// A person id or a line id: short, plain, never an object a writer slipped in.
|
|
169
|
+
const MAX_PERSON_CHARS = 64;
|
|
170
|
+
function shortId(value) {
|
|
171
|
+
return typeof value === 'string' && value && value.length <= MAX_PERSON_CHARS ? value : null;
|
|
172
|
+
}
|
|
173
|
+
const DAY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
174
|
+
function calendarDay(value) {
|
|
175
|
+
if (value === 'none') return 'none';
|
|
176
|
+
const m = typeof value === 'string' ? DAY_RE.exec(value) : null;
|
|
177
|
+
if (!m) return undefined;
|
|
178
|
+
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
179
|
+
const date = new Date(Date.UTC(y, mo - 1, d));
|
|
180
|
+
return date.getUTCFullYear() === y && date.getUTCMonth() === mo - 1 && date.getUTCDate() === d ? value : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
116
183
|
// How long a claim survives without a heartbeat. The MCP server heartbeats on a
|
|
117
184
|
// timer while it holds any claim, so this is a liveness signal about the PROCESS
|
|
118
185
|
// (session dies, child dies, heartbeats stop) rather than anything the model has
|
|
@@ -133,6 +200,9 @@ const MAX_SHORT = 200;
|
|
|
133
200
|
// see. Four fields at this size still leave a line at half the append ceiling.
|
|
134
201
|
export const MAX_TEXT_BYTES = 32 * 1024;
|
|
135
202
|
const MAX_LABELS = 20;
|
|
203
|
+
// How long one emoji may be, in code points. 👩🏽🚀 is five; eight leaves room
|
|
204
|
+
// for the longest sequence anyone picks and refuses a sentence pasted in.
|
|
205
|
+
const MAX_EMOJI = 8;
|
|
136
206
|
const MAX_ID_CHARS = 64;
|
|
137
207
|
// A single line longer than this is treated as torn or hostile and skipped
|
|
138
208
|
// rather than parsed. Matches the ceiling records.mjs already uses.
|
|
@@ -235,6 +305,11 @@ export function normalizeLine(line) {
|
|
|
235
305
|
id: raw.id,
|
|
236
306
|
ts: Number.isFinite(raw.ts) ? raw.ts : 0,
|
|
237
307
|
source,
|
|
308
|
+
// WHO WROTE IT and the line's own id. Stamped where a line reaches the
|
|
309
|
+
// disk on a signed-in Mac (main/store/work-items.mjs), absent everywhere
|
|
310
|
+
// else, and a line without them reads exactly as it always did.
|
|
311
|
+
by: shortId(raw.by),
|
|
312
|
+
uid: shortId(raw.uid),
|
|
238
313
|
// A founder line is never fenced, so its epoch is ignored even if present.
|
|
239
314
|
epoch: source === 'agent' && Number.isFinite(raw.epoch) ? raw.epoch : null,
|
|
240
315
|
claim: raw.claim && typeof raw.claim === 'object' ? raw.claim : null,
|
|
@@ -321,6 +396,12 @@ function coerceField(field, value) {
|
|
|
321
396
|
// that coerces to undefined is dropped by the fold, so without a falsy
|
|
322
397
|
// sentinel a schedule could be set and never cleared.
|
|
323
398
|
case 'runAt': return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
399
|
+
// An inbox reminder, independent of when work may start. 0 brings it back.
|
|
400
|
+
case 'snoozedUntil': return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : undefined;
|
|
401
|
+
// 'later' or 'now', and 'now' is the falsy sentinel for the same reason
|
|
402
|
+
// runAt keeps 0: a field that coerces to undefined is dropped by the fold,
|
|
403
|
+
// so without a real value for "started" nothing could ever clear it.
|
|
404
|
+
case 'start': return value === 'later' || value === 'now' ? value : undefined;
|
|
324
405
|
// WHICH ANSWER A SESSION HAS ALREADY ACTED ON, as that answer's own ts.
|
|
325
406
|
//
|
|
326
407
|
// An answer had two states and the ledger recorded one. A worker finishes
|
|
@@ -340,6 +421,40 @@ function coerceField(field, value) {
|
|
|
340
421
|
const labels = value.map((l) => str(l, MAX_SHORT)).filter(Boolean).slice(0, MAX_LABELS);
|
|
341
422
|
return labels.length ? labels : undefined;
|
|
342
423
|
}
|
|
424
|
+
case 'assignee': case 'runner': return shortId(value) ?? undefined;
|
|
425
|
+
case 'due': return calendarDay(value);
|
|
426
|
+
case 'people': {
|
|
427
|
+
if (!Array.isArray(value)) return undefined;
|
|
428
|
+
const people = [...new Set(value.map(shortId).filter(Boolean))].slice(0, MAX_LABELS);
|
|
429
|
+
return people.length ? people : undefined;
|
|
430
|
+
}
|
|
431
|
+
// WHICH MESSAGE AND WHICH EMOJI. `on` is a line uid, bounded like any other
|
|
432
|
+
// id; the emoji is counted in CHARACTERS rather than bytes, because a flag
|
|
433
|
+
// or a person with a skin tone is several code points joined and cutting one
|
|
434
|
+
// mid-sequence stores a glyph nobody chose.
|
|
435
|
+
case 'react': {
|
|
436
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
437
|
+
const on = shortId(value.on);
|
|
438
|
+
const emoji = typeof value.emoji === 'string' ? [...value.emoji.trim()].slice(0, MAX_EMOJI).join('') : '';
|
|
439
|
+
if (!on || !emoji) return undefined;
|
|
440
|
+
return value.off === true ? { on, emoji, off: true } : { on, emoji };
|
|
441
|
+
}
|
|
442
|
+
case 'inReplyTo': return shortId(value) ?? undefined;
|
|
443
|
+
case 'visibility': return value === 'private' || value === 'team' || value === 'people' ? value : undefined;
|
|
444
|
+
// An empty list is a real value: it is how the last chosen person comes
|
|
445
|
+
// off a thread, and the thread then reaches nobody until she names one.
|
|
446
|
+
case 'visibleTo': {
|
|
447
|
+
if (!Array.isArray(value)) return undefined;
|
|
448
|
+
return [...new Set(value.map(shortId).filter(Boolean))].slice(0, MAX_LABELS);
|
|
449
|
+
}
|
|
450
|
+
// An empty string is a real value here: it is how a person clears a line
|
|
451
|
+
// of the summary, and a field that coerces to undefined cannot be cleared.
|
|
452
|
+
case 'problem': case 'progress': case 'solution':
|
|
453
|
+
return typeof value === 'string' ? value.trim().slice(0, MAX_SUMMARY) : undefined;
|
|
454
|
+
case 'blockedBy': case 'blocks': {
|
|
455
|
+
if (!Array.isArray(value)) return undefined;
|
|
456
|
+
return [...new Set(value.map(shortId).filter(Boolean))].slice(0, MAX_LABELS);
|
|
457
|
+
}
|
|
343
458
|
default: return undefined;
|
|
344
459
|
}
|
|
345
460
|
}
|
|
@@ -355,10 +470,9 @@ function coerceField(field, value) {
|
|
|
355
470
|
// WHO An agent's `done` and the founder's archive are the same word for
|
|
356
471
|
// opposite events. Her own ask, ended by an agent, is an answer she has
|
|
357
472
|
// not received; ended by her, it is filed. the app's inbox could not tell
|
|
358
|
-
// them apart, so every answer to a question
|
|
359
|
-
// the archive and
|
|
360
|
-
//
|
|
361
|
-
// and all three had been answered within twenty minutes).
|
|
473
|
+
// them apart, so every answer to a question the founder asked went
|
|
474
|
+
// straight to the archive, and it reasonably looked as though the work
|
|
475
|
+
// never happened, so the same question got asked again and again.
|
|
362
476
|
// WHEN Two identical nudges are two asks. A delivery keyed on the words
|
|
363
477
|
// alone treats the second as already handled and swallows it.
|
|
364
478
|
//
|
|
@@ -367,6 +481,22 @@ function coerceField(field, value) {
|
|
|
367
481
|
// 2. skip an agent line fenced out by a newer epoch (the zombie writer)
|
|
368
482
|
// 3. apply the claim, if this line carries one
|
|
369
483
|
// 4. apply the patch field by field, founder outranking agent
|
|
484
|
+
// THE CHIPS UNDER A MESSAGE, out of the presses the ledger holds. Message by
|
|
485
|
+
// message, emoji by emoji, the people still on it — oldest press first, so the
|
|
486
|
+
// chips do not reorder themselves under somebody's pointer as a teammate's
|
|
487
|
+
// lines arrive. Returns null when every press has been taken back, which is
|
|
488
|
+
// what leaves a row with no `reactions` at all.
|
|
489
|
+
function rollUpReactions(presses) {
|
|
490
|
+
const live = [...presses.values()].filter((p) => !p.off).sort((a, b) => a.ts - b.ts);
|
|
491
|
+
if (!live.length) return null;
|
|
492
|
+
const out = {};
|
|
493
|
+
for (const press of live) {
|
|
494
|
+
const message = out[press.on] ?? (out[press.on] = {});
|
|
495
|
+
(message[press.emoji] ?? (message[press.emoji] = [])).push(press.who);
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
|
|
370
500
|
export function foldWorkItems(lines, now = Date.now()) {
|
|
371
501
|
const items = new Map();
|
|
372
502
|
|
|
@@ -379,10 +509,15 @@ export function foldWorkItems(lines, now = Date.now()) {
|
|
|
379
509
|
item = {
|
|
380
510
|
id: line.id, status: 'open', title: '', kind: '', labels: [],
|
|
381
511
|
priority: 0, epoch: 0, claim: null, createdAt: line.ts, updatedAt: line.ts,
|
|
382
|
-
wrote: {},
|
|
512
|
+
createdBy: line.by, wrote: {},
|
|
383
513
|
};
|
|
384
514
|
items.set(line.id, item);
|
|
385
515
|
}
|
|
516
|
+
// Who started the row is whoever wrote its EARLIEST line, not the first
|
|
517
|
+
// line this Mac happened to receive: a teammate's lines arrive late.
|
|
518
|
+
if (line.ts < item.createdAt || (line.ts === item.createdAt && !item.createdBy)) {
|
|
519
|
+
if (line.by) item.createdBy = line.by;
|
|
520
|
+
}
|
|
386
521
|
|
|
387
522
|
// Fencing. An agent line written under a superseded epoch is the whole
|
|
388
523
|
// reason this field exists: it is a live worker whose lease lapsed, and
|
|
@@ -392,7 +527,9 @@ export function foldWorkItems(lines, now = Date.now()) {
|
|
|
392
527
|
if (line.claim) {
|
|
393
528
|
// A claim at or above the current epoch takes the item. A heartbeat only
|
|
394
529
|
// extends a lease it still legitimately holds.
|
|
395
|
-
|
|
530
|
+
const renewsHeldClaim = !line.heartbeat || (item.claim
|
|
531
|
+
&& line.epoch === item.epoch && line.claim.holder === item.claim.holder);
|
|
532
|
+
if (line.epoch >= item.epoch && renewsHeldClaim) {
|
|
396
533
|
item.epoch = line.epoch;
|
|
397
534
|
item.claim = {
|
|
398
535
|
holder: String(line.claim.holder ?? ''),
|
|
@@ -412,16 +549,48 @@ export function foldWorkItems(lines, now = Date.now()) {
|
|
|
412
549
|
// are already in her ledger and will be there forever.
|
|
413
550
|
const patch = line.heartbeat ? {} : line.patch;
|
|
414
551
|
|
|
552
|
+
// WHO SAID WHAT, AND WHERE, for a conversation between people
|
|
553
|
+
// (w-920461cbe6). Every person's message, kept in a list of its own because
|
|
554
|
+
// the fold below keeps only the newest answer, and a busy chat needs to know
|
|
555
|
+
// who spoke last in the chat itself and in each thread. Only a person's own
|
|
556
|
+
// words count (a line with a writer), and only rows with people on them keep
|
|
557
|
+
// the result (see the end of the fold).
|
|
558
|
+
const words = typeof patch.answer === 'string' && patch.answer !== WITHDRAWN ? patch.answer
|
|
559
|
+
: typeof patch.body === 'string' ? patch.body : null;
|
|
560
|
+
if (line.by && words && words.trim()) {
|
|
561
|
+
(item.said ??= []).push({ uid: line.uid ?? null, by: line.by, ts: line.ts, text: words.trim().slice(0, TALK_TEXT), inReplyTo: patch.inReplyTo ?? null, body: typeof patch.answer !== 'string' });
|
|
562
|
+
}
|
|
563
|
+
|
|
415
564
|
for (const [field, value] of Object.entries(patch)) {
|
|
565
|
+
// A REACTION IS ACCUMULATED, NOT HELD. Every other field below keeps one
|
|
566
|
+
// value and the newest writer owns it; a reaction belongs to the person
|
|
567
|
+
// who pressed it, so two people on one message have to survive each other.
|
|
568
|
+
// The key is the message, the emoji and the person, and within that one
|
|
569
|
+
// key the newest press wins — by its own timestamp, because a page pulled
|
|
570
|
+
// from the cloud is not promised in the order anybody pressed anything.
|
|
571
|
+
if (field === 'react') {
|
|
572
|
+
if (!item.reacts) item.reacts = new Map();
|
|
573
|
+
const who = line.by || 'you';
|
|
574
|
+
const key = `${value.on}\u0000${value.emoji}\u0000${who}`;
|
|
575
|
+
const was = item.reacts.get(key);
|
|
576
|
+
if (!was || line.ts >= was.ts) {
|
|
577
|
+
item.reacts.set(key, { ts: line.ts, off: value.off === true, who, on: value.on, emoji: value.emoji });
|
|
578
|
+
}
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
// WHICH MESSAGE A REPLY ANSWERS belongs to that reply's line alone. Held
|
|
582
|
+
// as a value, the row would carry the newest reply's parent forever.
|
|
583
|
+
if (field === 'inReplyTo') continue;
|
|
416
584
|
const held = item.wrote[field];
|
|
417
585
|
// There used to be an exception here, for machinery spending a one-off
|
|
418
586
|
// permission mode over the founder's own grant. That field is not on the
|
|
419
587
|
// ledger any more (see the note beside WORK_ITEM_FIELDS), so
|
|
420
588
|
// the plain rule is the only rule: a weaker writer never overwrites a
|
|
421
589
|
// stronger one's value.
|
|
422
|
-
if (held && !beats(line, held)) continue;
|
|
590
|
+
if (held && !beats(line, held, field)) continue;
|
|
423
591
|
item[field] = value;
|
|
424
592
|
item.wrote[field] = { ts: line.ts, source: authorityOf(line, field, value) };
|
|
593
|
+
if (line.by) item.wrote[field].by = line.by;
|
|
425
594
|
}
|
|
426
595
|
|
|
427
596
|
item.updatedAt = Math.max(item.updatedAt, line.ts);
|
|
@@ -440,18 +609,68 @@ export function foldWorkItems(lines, now = Date.now()) {
|
|
|
440
609
|
for (const item of items.values()) {
|
|
441
610
|
item.claimExpired = !!(item.claim && item.claim.leaseUntil < now);
|
|
442
611
|
if (item.status === 'claimed' && (!item.claim || item.claimExpired)) item.status = 'open';
|
|
612
|
+
if (item.reacts) {
|
|
613
|
+
const chips = rollUpReactions(item.reacts);
|
|
614
|
+
delete item.reacts;
|
|
615
|
+
if (chips) item.reactions = chips;
|
|
616
|
+
}
|
|
617
|
+
if (item.said) {
|
|
618
|
+
const said = item.said;
|
|
619
|
+
delete item.said;
|
|
620
|
+
if (Array.isArray(item.people) && item.people.length) item.talk = talkOf(said);
|
|
621
|
+
}
|
|
443
622
|
}
|
|
444
623
|
|
|
445
624
|
return items;
|
|
446
625
|
}
|
|
447
626
|
|
|
627
|
+
// A reply taken back reads as this, and is nobody's news.
|
|
628
|
+
const WITHDRAWN = '(withdrawn)';
|
|
629
|
+
// How much of a message the record keeps: enough for a row's one line.
|
|
630
|
+
const TALK_TEXT = 280;
|
|
631
|
+
// How many of the newest messages the record keeps, in the chat and in each
|
|
632
|
+
// thread. The counts it answers stop at your own newest word, which in a chat
|
|
633
|
+
// you are part of is never this far back.
|
|
634
|
+
const TALK_KEEP = 100;
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* THE CHAT AND ITS THREADS, AS WHO SPOKE WHERE (w-920461cbe6): `chat` is the
|
|
638
|
+
* main conversation, oldest first, and `threads` the replies under each message
|
|
639
|
+
* they answer, with who wrote that message and its words. Pure bookkeeping for
|
|
640
|
+
* `whatWaits` (shared/team-rules.mjs) and the row's one line; the messages
|
|
641
|
+
* themselves are read off the ledger by the thread view, as before.
|
|
642
|
+
*/
|
|
643
|
+
function talkOf(said) {
|
|
644
|
+
const lines = [...said].sort((a, b) => a.ts - b.ts);
|
|
645
|
+
const chat = [];
|
|
646
|
+
const threads = {};
|
|
647
|
+
const parents = new Map();
|
|
648
|
+
const bodies = new Set();
|
|
649
|
+
for (const s of lines) {
|
|
650
|
+
// A rename that sends the same opening words again is not a new message.
|
|
651
|
+
if (s.body) { if (bodies.has(s.text)) continue; bodies.add(s.text); }
|
|
652
|
+
const said1 = { by: s.by, ts: s.ts, text: s.text };
|
|
653
|
+
const parent = s.inReplyTo ? parents.get(s.inReplyTo) : null;
|
|
654
|
+
if (parent) {
|
|
655
|
+
(threads[s.inReplyTo] ??= { by: parent.by, text: parent.text, replies: [] }).replies.push(said1);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
// A reply to a message this chat does not hold is drawn in the chat, so it
|
|
659
|
+
// counts there too.
|
|
660
|
+
chat.push(said1);
|
|
661
|
+
if (s.uid) parents.set(s.uid, said1);
|
|
662
|
+
}
|
|
663
|
+
for (const t of Object.values(threads)) t.replies = t.replies.slice(-TALK_KEEP);
|
|
664
|
+
return { chat: chat.slice(-TALK_KEEP), threads };
|
|
665
|
+
}
|
|
666
|
+
|
|
448
667
|
/* -------------------------------- the thread ----------------------------- */
|
|
449
668
|
// EVERY TURN OF ONE ROW, OLDEST FIRST. The fold answers "what does this item
|
|
450
669
|
// say now"; this answers "what has been said here", which is a different
|
|
451
670
|
// question and nothing could ask it before.
|
|
452
671
|
//
|
|
453
|
-
//
|
|
454
|
-
// holding one field, `answer`, with no sight of
|
|
672
|
+
// There was a real hole here. Every reply the user wrote spawned a fresh session
|
|
673
|
+
// holding one field, `answer`, with no sight of their earlier answers on the same
|
|
455
674
|
// row and no sight of the work it was replying about. One session built
|
|
456
675
|
// thirty-nine images off a bare line asking for more options, because cold that
|
|
457
676
|
// is what the line says. The whole exchange was on disk the entire time: the
|
|
@@ -512,7 +731,8 @@ function authorityOf(line, field, value) {
|
|
|
512
731
|
|
|
513
732
|
// Does the incoming line outrank the one already holding this field? The founder
|
|
514
733
|
// wins over any agent regardless of time; between equals, later wins.
|
|
515
|
-
function beats(incoming, held) {
|
|
734
|
+
function beats(incoming, held, field) {
|
|
735
|
+
if (SHARED_FIELDS.has(field)) return incoming.ts >= held.ts;
|
|
516
736
|
if (held.source === 'founder' && incoming.source !== 'founder') return false;
|
|
517
737
|
if (incoming.source === 'founder' && held.source !== 'founder') return true;
|
|
518
738
|
return incoming.ts >= held.ts;
|
|
@@ -530,10 +750,20 @@ function beats(incoming, held) {
|
|
|
530
750
|
// the whole reason this is a predicate over the clock rather than a timer, and
|
|
531
751
|
// it is what makes a schedule survive an app restart, a reboot, and a store
|
|
532
752
|
// restored onto another machine (the founder's requirement, 2026-08-06).
|
|
753
|
+
// AND A THREAD IN LATER WAITS FOR A PERSON RATHER THAN A CLOCK (w-afb66e6661).
|
|
754
|
+
// "Add it to Later" on the card writes `start: 'later'`, which means exactly
|
|
755
|
+
// what a future moment means and never arrives on its own: not claimable, no
|
|
756
|
+
// session, not in the inbox. Pressing start on the thread writes 'now'.
|
|
533
757
|
export function isDue(item, now = Date.now()) {
|
|
758
|
+
if (item?.start === 'later') return false;
|
|
534
759
|
return !item?.runAt || item.runAt <= now;
|
|
535
760
|
}
|
|
536
761
|
|
|
762
|
+
/** Written down and deliberately not begun. */
|
|
763
|
+
export function notStarted(item) {
|
|
764
|
+
return item?.start === 'later';
|
|
765
|
+
}
|
|
766
|
+
|
|
537
767
|
export function isClaimable(item, now = Date.now()) {
|
|
538
768
|
if (!item) return false;
|
|
539
769
|
if (!PULLABLE.includes(item.status)) return false;
|