@namzu/sdk 52.7.0 → 53.1.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/CHANGELOG.md +45 -26
- package/dist/authorization/protected-paths.d.ts +2 -4
- package/dist/authorization/protected-paths.d.ts.map +1 -1
- package/dist/authorization/protected-paths.js +2 -4
- package/dist/authorization/protected-paths.js.map +1 -1
- package/dist/authorization/readonly-commands.d.ts +4 -4
- package/dist/authorization/readonly-commands.js +7 -7
- package/dist/authorization/readonly-commands.js.map +1 -1
- package/dist/authorization/readonly-flags.d.ts +1 -2
- package/dist/authorization/readonly-flags.d.ts.map +1 -1
- package/dist/authorization/readonly-flags.js +1 -2
- package/dist/authorization/readonly-flags.js.map +1 -1
- package/dist/authorization/removal.d.ts +2 -3
- package/dist/authorization/removal.d.ts.map +1 -1
- package/dist/authorization/removal.js +3 -4
- package/dist/authorization/removal.js.map +1 -1
- package/dist/authorization/shell-paths.d.ts +5 -5
- package/dist/authorization/shell-paths.js +6 -6
- package/dist/authorization/shell-paths.js.map +1 -1
- package/dist/compaction/context-window.d.ts +1 -1
- package/dist/compaction/context-window.js +1 -1
- package/dist/compaction/verifier.js +2 -2
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/connector/web/page.d.ts +2 -3
- package/dist/connector/web/page.d.ts.map +1 -1
- package/dist/connector/web/page.js +2 -3
- package/dist/connector/web/page.js.map +1 -1
- package/dist/execution/workflow/prelude.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.js +29 -5
- package/dist/execution/workflow/prelude.js.map +1 -1
- package/dist/execution/workflow/runtime.d.ts +2 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +8 -3
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +3 -3
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +1 -1
- package/dist/manager/agent/lifecycle.js +1 -1
- package/dist/plugin/shell-hook.d.ts +1 -1
- package/dist/plugin/shell-hook.js +4 -4
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/provider/errors.js +2 -2
- package/dist/provider/errors.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/jobs/monitor-stream.d.ts +1 -2
- package/dist/runtime/jobs/monitor-stream.d.ts.map +1 -1
- package/dist/runtime/jobs/monitor-stream.js +1 -2
- package/dist/runtime/jobs/monitor-stream.js.map +1 -1
- package/dist/runtime/jobs/websocket-source.d.ts +1 -1
- package/dist/runtime/jobs/websocket-source.d.ts.map +1 -1
- package/dist/runtime/jobs/websocket-source.js +1 -1
- package/dist/runtime/jobs/websocket-source.js.map +1 -1
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +3 -1
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/iteration/index.js +3 -3
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +10 -2
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +2 -2
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +2 -2
- package/dist/runtime/query/iteration/phases/compaction.d.ts +3 -5
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +6 -8
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/request-repair.d.ts +1 -1
- package/dist/runtime/query/iteration/request-repair.js +2 -2
- package/dist/runtime/query/iteration/request-repair.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts.map +1 -1
- package/dist/runtime/query/path-escalation.js +2 -3
- package/dist/runtime/query/path-escalation.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts +21 -1
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +20 -1
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +1 -1
- package/dist/store/checkpoint/memory.d.ts +1 -1
- package/dist/store/checkpoint/memory.js +1 -1
- package/dist/store/memory/markdown-format.d.ts +1 -1
- package/dist/store/memory/markdown-format.js +1 -1
- package/dist/store/memory/markdown.js +1 -1
- package/dist/store/session-log/core.d.ts.map +1 -1
- package/dist/store/session-log/core.js +28 -0
- package/dist/store/session-log/core.js.map +1 -1
- package/dist/store/session-log/spilled-input.d.ts +34 -0
- package/dist/store/session-log/spilled-input.d.ts.map +1 -0
- package/dist/store/session-log/spilled-input.js +68 -0
- package/dist/store/session-log/spilled-input.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +1 -2
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit-match.d.ts +5 -6
- package/dist/tools/builtins/edit-match.d.ts.map +1 -1
- package/dist/tools/builtins/edit-match.js +5 -6
- package/dist/tools/builtins/edit-match.js.map +1 -1
- package/dist/types/provider/errors.js +1 -1
- package/dist/types/provider/errors.js.map +1 -1
- package/package.json +1 -1
- package/src/authorization/protected-paths.ts +2 -4
- package/src/authorization/readonly-commands.ts +7 -7
- package/src/authorization/readonly-flags.ts +1 -2
- package/src/authorization/removal.ts +3 -4
- package/src/authorization/shell-paths.ts +6 -6
- package/src/compaction/context-window.ts +1 -1
- package/src/compaction/verifier.ts +2 -2
- package/src/connector/web/page.ts +2 -3
- package/src/execution/workflow/prelude.ts +29 -5
- package/src/execution/workflow/runtime.ts +9 -3
- package/src/execution/workflow/types.ts +3 -3
- package/src/manager/agent/lifecycle.ts +1 -1
- package/src/plugin/shell-hook.ts +4 -4
- package/src/provider/errors.ts +2 -2
- package/src/public-runtime.ts +1 -0
- package/src/runtime/jobs/monitor-stream.ts +1 -2
- package/src/runtime/jobs/websocket-source.ts +1 -1
- package/src/runtime/query/checkpoint.ts +6 -1
- package/src/runtime/query/iteration/index.ts +3 -3
- package/src/runtime/query/iteration/outstanding-work.ts +9 -2
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +2 -2
- package/src/runtime/query/iteration/phases/compaction.ts +6 -8
- package/src/runtime/query/iteration/request-repair.ts +3 -3
- package/src/runtime/query/path-escalation.ts +2 -3
- package/src/scheduler/completion-inbox.ts +31 -1
- package/src/session/workspace/driver.ts +1 -1
- package/src/store/checkpoint/memory.ts +1 -1
- package/src/store/memory/markdown-format.ts +1 -1
- package/src/store/memory/markdown.ts +1 -1
- package/src/store/session-log/core.ts +28 -0
- package/src/store/session-log/spilled-input.ts +90 -0
- package/src/tools/builtins/bash.ts +1 -2
- package/src/tools/builtins/edit-match.ts +5 -6
- package/src/types/provider/errors.ts +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which shell lines only read, so that they run without a prompt.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `ls`, `cat`, `git status` and `git diff` run without asking, because each is
|
|
5
|
+
* on a vetted list and nothing in the line is decided at run time. The list and
|
|
6
|
+
* the conditions are these:
|
|
7
7
|
*
|
|
8
8
|
* - every command of the line is on the list, the line parses completely, and
|
|
9
9
|
* nothing in it is read at run time: no `$var` outside single quotes, no
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Not ported, and so never read-only here: `sed`, `ps`, `lsof`, `ss`, `date`,
|
|
24
24
|
* `man`, `xargs`, `fd`, `gh`, `docker logs|inspect`, `printf`, `[[`, `test`
|
|
25
|
-
* and the shell builtins other than `cd`. The
|
|
25
|
+
* and the shell builtins other than `cd`. The check of the working
|
|
26
26
|
* directory for a planted bare repository needs the directory, which the tool
|
|
27
27
|
* predicate does not get; the trusted-project gate is what stands in for it.
|
|
28
28
|
*
|
|
@@ -35,7 +35,7 @@ import type { ShellDialect } from '../types/tool/index.js'
|
|
|
35
35
|
import { FLAG_TABLES, type FlagTable } from './readonly-flags.js'
|
|
36
36
|
import { type ShellWord, lexShellCommandLine } from './shell-lexer.js'
|
|
37
37
|
|
|
38
|
-
/** Lines longer than this are not analysed (the
|
|
38
|
+
/** Lines longer than this are not analysed (the budget). */
|
|
39
39
|
const MAX_LINE_LENGTH = 20_000
|
|
40
40
|
|
|
41
41
|
/** Variables whose value cannot change what a command does. */
|
|
@@ -80,7 +80,7 @@ const SAFE_ENVIRONMENT: ReadonlySet<string> = new Set([
|
|
|
80
80
|
'GIT_TERMINAL_PROMPT',
|
|
81
81
|
])
|
|
82
82
|
|
|
83
|
-
/** Commands that read and print, with any arguments (the
|
|
83
|
+
/** Commands that read and print, with any arguments (the bare-safe list). */
|
|
84
84
|
const ANY_ARGUMENTS: ReadonlySet<string> = new Set([
|
|
85
85
|
'cal',
|
|
86
86
|
'uptime',
|
|
@@ -570,7 +570,7 @@ const GIT_CHECKS: Readonly<Record<string, Dangerous>> = {
|
|
|
570
570
|
},
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
-
// -- the commands with a regex of their own
|
|
573
|
+
// -- the commands with a regex of their own ----------------
|
|
574
574
|
|
|
575
575
|
function uniqIsReadOnly(args: readonly string[]): boolean {
|
|
576
576
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-flag allowlists for the shell commands that may run without a prompt.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* flags it vets and what each takes, `none` (a switch), `string` or `number`.
|
|
4
|
+
* For each command, the flags it vets and what each takes, `none` (a switch), `string` or `number`.
|
|
6
5
|
* A flag not listed here is not read-only, which is how `git diff --output=f`
|
|
7
6
|
* (writes a file), `git -c`, `--exec-path` and `--config-env` stay out: they
|
|
8
7
|
* are not in any table. Space-separated lists per kind keep the file readable;
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* `rm -rf ~`, `rm -rf $HOME`, `rm -rf "$BUILD/"` (empty variable, so `/`) and
|
|
8
8
|
* `rm -rf ..` all went past it. The flags were never the point. A removal is
|
|
9
9
|
* catastrophic because of WHAT it removes, so this reads the targets of `rm`
|
|
10
|
-
* and `rmdir` whatever the flags
|
|
11
|
-
* check does (version 2.1.302).
|
|
10
|
+
* and `rmdir` whatever the flags.
|
|
12
11
|
*
|
|
13
12
|
* A removal is flagged when a target is
|
|
14
13
|
*
|
|
@@ -26,7 +25,7 @@
|
|
|
26
25
|
*
|
|
27
26
|
* The answer is a reason, which the kernel turns into an escalation: asked
|
|
28
27
|
* every time, in every mode, and refused when nobody can be asked. It is not a
|
|
29
|
-
* denial:
|
|
28
|
+
* denial: it asks, because there are legitimate removals a person
|
|
30
29
|
* can approve.
|
|
31
30
|
*/
|
|
32
31
|
|
|
@@ -250,7 +249,7 @@ function checkTarget(
|
|
|
250
249
|
const expanded = expandTilde(spelled, scope.home)
|
|
251
250
|
const relative = !isAbsolute(expanded)
|
|
252
251
|
if (expandsAtRuntime(word) && /[$`]/.test(expanded)) {
|
|
253
|
-
// A variable the check cannot read:
|
|
252
|
+
// A variable the check cannot read: it is treated as a relative
|
|
254
253
|
// name, which is also what the shell does when the variable is unset and
|
|
255
254
|
// the rest is a plain name. Nothing more to learn from it.
|
|
256
255
|
return undefined
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
* and `echo x > ~/.profile` carry their paths inside one string, and a
|
|
7
7
|
* boundary that only reads declared arguments let both through.
|
|
8
8
|
*
|
|
9
|
-
* This
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* This reads the paths a shell line touches: a table of the commands whose
|
|
10
|
+
* positional arguments are paths, with an extractor for each command, the kind
|
|
11
|
+
* of access each makes (read, write or create), and the output redirections of
|
|
12
|
+
* the line. It is a
|
|
13
13
|
* reading of the text, not a model of the shell:
|
|
14
14
|
*
|
|
15
15
|
* - a word that expands at runtime (`$VAR`, `$(...)`) has no path to read;
|
|
16
16
|
* it is reported with `unresolved` so the caller decides what an unreadable
|
|
17
|
-
* write target is worth
|
|
17
|
+
* write target is worth;
|
|
18
18
|
* - a `cd` earlier in the line moves every relative path after it, which the
|
|
19
19
|
* reading does not follow, so the caller is told one happened;
|
|
20
20
|
* - a command that is not in the table contributes nothing.
|
|
@@ -279,7 +279,7 @@ interface FlagTable {
|
|
|
279
279
|
readonly valuedLong: ReadonlySet<string>
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
/** Operands of a command whose flags are known: an unknown flag stops flag parsing
|
|
282
|
+
/** Operands of a command whose flags are known: an unknown flag stops flag parsing. */
|
|
283
283
|
function operandsByTable(table: FlagTable): (args: Args) => ShellWord[] {
|
|
284
284
|
const kind = (flag: string): 'boolean' | 'valued' | 'attached' | 'unknown' => {
|
|
285
285
|
if (flag.startsWith('--')) {
|
|
@@ -121,7 +121,7 @@ export const MIN_DERIVED_TRIGGER_FRACTION = 0.5
|
|
|
121
121
|
* The fraction of the window at which older history is summarised.
|
|
122
122
|
*
|
|
123
123
|
* A host that set `triggerThreshold` gets exactly that. Otherwise the
|
|
124
|
-
* trigger is the
|
|
124
|
+
* trigger is the window, less room kept for the
|
|
125
125
|
* reply (the model's output cap, at most 20,000 tokens), less a 13,000-token
|
|
126
126
|
* buffer. On a 200,000-token window that is 167,000 tokens (83.5%); on a
|
|
127
127
|
* 1,000,000-token window, 967,000 (96.7%).
|
|
@@ -255,8 +255,8 @@ async function buildVerifiedSummaryWithProvider(
|
|
|
255
255
|
return serialized
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
// The verifier's own request can overflow a small window.
|
|
259
|
-
//
|
|
258
|
+
// The verifier's own request can overflow a small window. That is answered
|
|
259
|
+
// with a ladder: send less, and when nothing is left to
|
|
260
260
|
// cut, work from everything it already has. Here "less" is the older half of
|
|
261
261
|
// the excerpt dropped behind a placeholder, at most three times, and
|
|
262
262
|
// "everything it already has" is the extracted state, which the verifier only
|
|
@@ -16,14 +16,13 @@ export const WEB_PAGE_CACHE_MAX_BYTES = 50 * 1024 * 1024
|
|
|
16
16
|
export const WEB_PAGE_CONVERT_MAX_CHARS = 1024 * 1024
|
|
17
17
|
/** Most page text a model is asked about in one call. */
|
|
18
18
|
export const WEB_PAGE_MODEL_MAX_CHARS = 100_000
|
|
19
|
-
/** Marker
|
|
19
|
+
/** Marker that truncated content ends with. */
|
|
20
20
|
export const WEB_PAGE_TRUNCATED_MARKER = '[Content truncated due to length...]'
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* A redirect stays on the site when only the path or query changes (and
|
|
24
24
|
* `www.` comes or goes). `http:` to `https:` on the same host and default
|
|
25
|
-
* port is the same site too
|
|
26
|
-
* before it asks, so that hop never reaches it.
|
|
25
|
+
* port is the same site too.
|
|
27
26
|
*/
|
|
28
27
|
export function isSameSiteRedirect(from: URL, to: URL): boolean {
|
|
29
28
|
const upgrade = from.protocol === 'http:' && to.protocol === 'https:'
|
|
@@ -44,6 +44,15 @@ const PRELUDE_SOURCE = String.raw`(function (config) {
|
|
|
44
44
|
if (t === 'object') return typeof value.then === 'function' ? 'a promise' : 'an object';
|
|
45
45
|
return 'a ' + t;
|
|
46
46
|
}
|
|
47
|
+
function overBudget() {
|
|
48
|
+
return config.tokenLimit !== null && config.tokenLimit > 0 && spent.tokensUsed >= config.tokenLimit;
|
|
49
|
+
}
|
|
50
|
+
function budgetError() {
|
|
51
|
+
var e = new Error('Workflow token budget exceeded (' + spent.tokensUsed.toLocaleString('en-US') + ' / ' + config.tokenLimit.toLocaleString('en-US') + ' output tokens). Stopping further agent() calls. Agents already running will finish and their results are kept.');
|
|
52
|
+
e.name = 'WorkflowBudgetExceededError';
|
|
53
|
+
return e;
|
|
54
|
+
}
|
|
55
|
+
function isBudgetError(e) { return !!e && e.name === 'WorkflowBudgetExceededError'; }
|
|
47
56
|
async function agent(prompt, opts) {
|
|
48
57
|
if (typeof prompt !== 'string' || prompt.length === 0) {
|
|
49
58
|
throw new TypeError('agent(prompt, options?): the prompt must be a non-empty string, got ' + kind(prompt));
|
|
@@ -52,6 +61,7 @@ const PRELUDE_SOURCE = String.raw`(function (config) {
|
|
|
52
61
|
if (typeof o !== 'object' || Array.isArray(o)) {
|
|
53
62
|
throw new TypeError('agent(prompt, options?): options must be an object, got ' + kind(o));
|
|
54
63
|
}
|
|
64
|
+
if (overBudget()) throw budgetError();
|
|
55
65
|
var request = { prompt: prompt, cursor: cursor };
|
|
56
66
|
if (o.label != null) request.label = clean(o.label, 120);
|
|
57
67
|
if (o.phase != null) request.phase = clean(o.phase, 120);
|
|
@@ -72,12 +82,16 @@ const PRELUDE_SOURCE = String.raw`(function (config) {
|
|
|
72
82
|
throw new TypeError('parallel(thunks): item ' + (p + 1) + ' must be a function (wrap the work as () => agent(...)), got ' + kind(thunks[p]));
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
|
-
|
|
85
|
+
var dropped = 0;
|
|
86
|
+
var results = await Promise.all(thunks.map(function (t, n) {
|
|
76
87
|
return Promise.resolve().then(t).catch(function (e) {
|
|
77
|
-
|
|
88
|
+
if (isBudgetError(e)) { dropped++; return null; }
|
|
89
|
+
log('Step ' + (n + 1) + ' stopped: ' + clean(e && e.message ? e.message : e, 300));
|
|
78
90
|
return null;
|
|
79
91
|
});
|
|
80
92
|
}));
|
|
93
|
+
if (dropped > 0) log('parallel: ' + dropped + (dropped === 1 ? ' item' : ' items') + ' dropped because the token budget is spent.');
|
|
94
|
+
return results;
|
|
81
95
|
}
|
|
82
96
|
async function pipeline(items) {
|
|
83
97
|
if (!Array.isArray(items)) {
|
|
@@ -89,21 +103,31 @@ const PRELUDE_SOURCE = String.raw`(function (config) {
|
|
|
89
103
|
throw new TypeError('pipeline(items, ...stages): stage ' + (s + 1) + ' must be a function, got ' + kind(stages[s]));
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
|
-
|
|
106
|
+
if (items.length > 0 && overBudget()) throw budgetError();
|
|
107
|
+
var dropped = 0;
|
|
108
|
+
var results = await Promise.all(items.map(async function (item, index) {
|
|
93
109
|
var value = item;
|
|
94
110
|
for (var i = 0; i < stages.length; i++) {
|
|
95
111
|
try {
|
|
96
|
-
value = await stages[i](value,
|
|
112
|
+
value = await stages[i](value, item, index);
|
|
97
113
|
} catch (e) {
|
|
98
|
-
|
|
114
|
+
if (isBudgetError(e)) { dropped++; return null; }
|
|
115
|
+
log('Item ' + (index + 1) + ' stopped at step ' + (i + 1) + ': ' + clean(e && e.message ? e.message : e, 300));
|
|
99
116
|
return null;
|
|
100
117
|
}
|
|
101
118
|
if (value === null || value === undefined) return null;
|
|
102
119
|
}
|
|
103
120
|
return value;
|
|
104
121
|
}));
|
|
122
|
+
if (dropped > 0) log('pipeline: ' + dropped + (dropped === 1 ? ' item' : ' items') + ' dropped because the token budget is spent.');
|
|
123
|
+
return results;
|
|
105
124
|
}
|
|
106
125
|
var budget = Object.freeze({
|
|
126
|
+
get total() { return config.tokenLimit; },
|
|
127
|
+
spent: function () { return spent.tokensUsed; },
|
|
128
|
+
remaining: function () {
|
|
129
|
+
return config.tokenLimit === null ? Infinity : Math.max(0, config.tokenLimit - spent.tokensUsed);
|
|
130
|
+
},
|
|
107
131
|
get tokensUsed() { return spent.tokensUsed; },
|
|
108
132
|
get tokenLimit() { return config.tokenLimit === null ? undefined : config.tokenLimit; },
|
|
109
133
|
});
|
|
@@ -46,6 +46,12 @@ const AGENT_CALL_KEYS = new Set([
|
|
|
46
46
|
'effort',
|
|
47
47
|
])
|
|
48
48
|
|
|
49
|
+
/** The row title of an agent the script did not label: the first 60 characters of its prompt, whitespace squeezed. */
|
|
50
|
+
export function defaultAgentLabel(prompt: string, index: number): string {
|
|
51
|
+
const label = prompt.slice(0, 60).replace(/\s+/g, ' ').trim()
|
|
52
|
+
return label.length > 0 ? label : `Agent ${index}`
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
function integer(name: string, value: number, min: number, max: number): number {
|
|
50
56
|
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
51
57
|
throw new RangeError(`${name} must be an integer between ${min} and ${max}.`)
|
|
@@ -54,10 +60,10 @@ function integer(name: string, value: number, min: number, max: number): number
|
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
export function resolveWorkflowLimits(limits: WorkflowLimits = {}): WorkflowResolvedLimits {
|
|
57
|
-
const defaultConcurrency = Math.min(
|
|
63
|
+
const defaultConcurrency = Math.min(16, Math.max(2, cpus().length - 2))
|
|
58
64
|
return {
|
|
59
65
|
maxConcurrency: integer('maxConcurrency', limits.maxConcurrency ?? defaultConcurrency, 1, 16),
|
|
60
|
-
maxAgents: integer('maxAgents', limits.maxAgents ??
|
|
66
|
+
maxAgents: integer('maxAgents', limits.maxAgents ?? 1000, 1, 1000),
|
|
61
67
|
maxSourceBytes: integer(
|
|
62
68
|
'maxSourceBytes',
|
|
63
69
|
limits.maxSourceBytes ?? 256 * 1024,
|
|
@@ -502,7 +508,7 @@ export class WorkflowRuntime {
|
|
|
502
508
|
const queuedAt = now()
|
|
503
509
|
const agent: AgentRecord = {
|
|
504
510
|
index,
|
|
505
|
-
label: call.label ||
|
|
511
|
+
label: call.label || defaultAgentLabel(call.prompt, index),
|
|
506
512
|
prompt: call.prompt,
|
|
507
513
|
...(phase ? { phaseIndex: phase.index, phaseTitle: phase.title } : {}),
|
|
508
514
|
...(call.agentType !== undefined ? { agentType: call.agentType } : {}),
|
|
@@ -232,9 +232,9 @@ export interface WorkflowJournal {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
export interface WorkflowLimits {
|
|
235
|
-
/** Agents running at once. Default `min(
|
|
235
|
+
/** Agents running at once. Default `min(16, cpus - 2)`, at least 2; at most 16. */
|
|
236
236
|
readonly maxConcurrency?: number
|
|
237
|
-
/** Agents one run may start. Default
|
|
237
|
+
/** Agents one run may start. Default 1000; at most 1000. */
|
|
238
238
|
readonly maxAgents?: number
|
|
239
239
|
/** UTF-8 script size. Default 256 KiB. */
|
|
240
240
|
readonly maxSourceBytes?: number
|
|
@@ -242,7 +242,7 @@ export interface WorkflowLimits {
|
|
|
242
242
|
readonly memoryLimitBytes?: number
|
|
243
243
|
/** One value crossing the bridge, either way. Default 1 MiB. */
|
|
244
244
|
readonly maxValueBytes?: number
|
|
245
|
-
/** Stop admitting new agents once this many tokens are spent. Default: none. */
|
|
245
|
+
/** Stop admitting new agents once this many tokens are spent; also the script's `budget.total`. Default: none. */
|
|
246
246
|
readonly tokenBudget?: number
|
|
247
247
|
/** Wall clock for the whole run. Default 6 hours. */
|
|
248
248
|
readonly maxDurationMs?: number
|
|
@@ -348,7 +348,7 @@ export class AgentManager {
|
|
|
348
348
|
|
|
349
349
|
/**
|
|
350
350
|
* The refusal for a delegation past the depth limit. It tells the model what
|
|
351
|
-
* to do instead
|
|
351
|
+
* to do instead: a bare limit leaves it
|
|
352
352
|
* retrying the same spawn, while an instruction to do the work itself ends the
|
|
353
353
|
* attempt. The limit is `AgentManagerConfig.maxDepth`; there is no env override.
|
|
354
354
|
*/
|
package/src/plugin/shell-hook.ts
CHANGED
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
* - `systemMessage` — a warning for the operator, reported through the logger.
|
|
58
58
|
* - `suppressOutput` — keep this hook's stdout out of the debug log.
|
|
59
59
|
* - `hookSpecificOutput` with `hookEventName` naming this event (Namzu's
|
|
60
|
-
* name or the
|
|
60
|
+
* name or the PascalCase spelling `PreToolUse`, `Stop`, ...):
|
|
61
61
|
* `permissionDecision` (`allow`, `deny` or `ask`) with
|
|
62
62
|
* `permissionDecisionReason`, `updatedInput` and `additionalContext` for
|
|
63
63
|
* `pre_tool_use`; `additionalContext` for `user_prompt_submit`,
|
|
@@ -458,8 +458,8 @@ function pathOf(toolInput: unknown): string | undefined {
|
|
|
458
458
|
return undefined
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
/** The
|
|
462
|
-
const
|
|
461
|
+
/** The PascalCase spelling of each event name, which a hook's JSON may use. */
|
|
462
|
+
const PASCAL_EVENT_NAMES: Readonly<Partial<Record<ShellHookEvent, string>>> = {
|
|
463
463
|
user_prompt_submit: 'UserPromptSubmit',
|
|
464
464
|
session_start: 'SessionStart',
|
|
465
465
|
session_end: 'SessionEnd',
|
|
@@ -565,7 +565,7 @@ function readShellHookJson(event: ShellHookEvent, stdout: string): ShellHookRead
|
|
|
565
565
|
if (typeof named !== 'string') {
|
|
566
566
|
return { invalid: '`hookSpecificOutput` is missing its `hookEventName`' }
|
|
567
567
|
}
|
|
568
|
-
if (named !== event && named !==
|
|
568
|
+
if (named !== event && named !== PASCAL_EVENT_NAMES[event]) {
|
|
569
569
|
return {
|
|
570
570
|
invalid: `\`hookSpecificOutput.hookEventName\` is "${named}" but this hook ran for ${event}`,
|
|
571
571
|
}
|
package/src/provider/errors.ts
CHANGED
|
@@ -260,8 +260,8 @@ const OVERFLOW_BODY_PATTERNS: readonly RegExp[] = [
|
|
|
260
260
|
// length" — so a pattern anchored on "exceeded" misses it entirely.
|
|
261
261
|
/exceeds?\s+(?:the\s+)?(?:model'?s?\s+)?context\s+(?:length|window|size)/i,
|
|
262
262
|
/context\s+(?:length|window|size)\s+exceed/i,
|
|
263
|
-
// The
|
|
264
|
-
// window". Here it must sit next to a verb that says the request did not
|
|
263
|
+
// The bare phrase "context
|
|
264
|
+
// window" is too broad on its own. Here it must sit next to a verb that says the request did not
|
|
265
265
|
// fit ("larger than the model context window of N tokens"), so a 400 that
|
|
266
266
|
// only names the window ("does not support this parameter with the 1M
|
|
267
267
|
// context window beta") stays an invalid request. The same pattern lives in
|
package/src/public-runtime.ts
CHANGED
|
@@ -722,6 +722,7 @@ export {
|
|
|
722
722
|
CompletionInbox,
|
|
723
723
|
formatCompletionNotification,
|
|
724
724
|
} from './scheduler/completion-inbox.js'
|
|
725
|
+
export type { CompletionInboxOptions } from './scheduler/completion-inbox.js'
|
|
725
726
|
|
|
726
727
|
// Scheduled jobs: WHEN something is due, and nothing about how a host stores
|
|
727
728
|
// or runs a job. The time engine and the evaluator are pure (`Intl` for time
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
* work without any injection), which is what lets the tests advance a window
|
|
16
16
|
* by calling a function rather than by waiting for one.
|
|
17
17
|
*
|
|
18
|
-
* The constants and the wording
|
|
19
|
-
* tool; deviations are named where they are made.
|
|
18
|
+
* The constants and the wording are fixed here.
|
|
20
19
|
*/
|
|
21
20
|
|
|
22
21
|
/** A longer line keeps its first this-many characters. */
|
|
@@ -29,7 +29,7 @@ import type { JobHost, JobSource } from './registry.js'
|
|
|
29
29
|
* asks a person first (see `tools/builtins/monitor.ts`).
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
/** The largest frame accepted; a bigger one ends the watch.
|
|
32
|
+
/** The largest frame accepted; a bigger one ends the watch. */
|
|
33
33
|
export const WEBSOCKET_MAX_PAYLOAD_BYTES = 1_048_576
|
|
34
34
|
export const WEBSOCKET_HANDSHAKE_TIMEOUT_MS = 15_000
|
|
35
35
|
/** After a polite close, how long a peer gets to answer before the socket is destroyed. */
|
|
@@ -2,6 +2,7 @@ import type { WorkingStateSnapshot } from '../../compaction/wire.js'
|
|
|
2
2
|
import { type TurnRecorder, readFoldedHistory } from '../../manager/session/turn-recorder.js'
|
|
3
3
|
import type { CheckpointScope, SessionCheckpointStore } from '../../store/checkpoint/index.js'
|
|
4
4
|
import type { SessionLog } from '../../store/session-log/index.js'
|
|
5
|
+
import { restoreSpilledRequestInputs } from '../../store/session-log/spilled-input.js'
|
|
5
6
|
import type { SerializedSpanContext } from '../../telemetry/attributes.js'
|
|
6
7
|
import { NamzuError } from '../../types/errors/index.js'
|
|
7
8
|
import type {
|
|
@@ -148,7 +149,11 @@ export async function readParks(
|
|
|
148
149
|
decisionId: r.decisionId,
|
|
149
150
|
turnId: r.turnId as TurnId,
|
|
150
151
|
checkpointId: r.checkpointId,
|
|
151
|
-
|
|
152
|
+
// A call input too large for a line was spilled; put it back.
|
|
153
|
+
request: (await restoreSpilledRequestInputs(
|
|
154
|
+
structuredClone(r.request) as unknown as HITLDecisionRequest & { toolCalls?: unknown },
|
|
155
|
+
(ref) => log.readSpill(ref),
|
|
156
|
+
)) as HITLDecisionRequest,
|
|
152
157
|
parkedAt: Date.parse(r.ts),
|
|
153
158
|
...(r.deadlineAt !== undefined ? { deadlineAt: Date.parse(r.deadlineAt) } : {}),
|
|
154
159
|
})
|
|
@@ -526,7 +526,7 @@ export class IterationOrchestrator {
|
|
|
526
526
|
await this.ctx.checkpointMgr.markLoopStart?.(recorder)
|
|
527
527
|
|
|
528
528
|
// Context-overflow relief is a two-rung ladder per *stuck point*, not per
|
|
529
|
-
// turn
|
|
529
|
+
// turn (at most two reactive compactions per
|
|
530
530
|
// overflow): the first relief, then one deeper pass if the retry still
|
|
531
531
|
// overflows.
|
|
532
532
|
//
|
|
@@ -2121,7 +2121,7 @@ export class IterationOrchestrator {
|
|
|
2121
2121
|
classifyProviderError(err, this.ctx.provider.id).code === 'context_length_exceeded'
|
|
2122
2122
|
) {
|
|
2123
2123
|
// The rapid-refill check comes first, before the first reactive relief
|
|
2124
|
-
// of an overflow
|
|
2124
|
+
// of an overflow. A relief that follows
|
|
2125
2125
|
// the previous compaction within the refill window counts toward the
|
|
2126
2126
|
// limit, so a tool that re-fills the window every time it is called
|
|
2127
2127
|
// stops the turn instead of being cleared and called again without end.
|
|
@@ -2155,7 +2155,7 @@ export class IterationOrchestrator {
|
|
|
2155
2155
|
continue
|
|
2156
2156
|
}
|
|
2157
2157
|
// Out of rungs. The conversation is kept; say why compaction could not
|
|
2158
|
-
// make room when it errored
|
|
2158
|
+
// make room when it errored.
|
|
2159
2159
|
const cause = compactionErrorNote(this.ctx)
|
|
2160
2160
|
if (cause !== undefined && err instanceof Error) {
|
|
2161
2161
|
try {
|
|
@@ -163,7 +163,7 @@ export async function* holdForOutstandingWork(
|
|
|
163
163
|
hasToolCalls: boolean,
|
|
164
164
|
deliverInbound: () => number | Promise<number>,
|
|
165
165
|
): AsyncGenerator<SessionEvent, boolean> {
|
|
166
|
-
const inbox = ctx.completionInbox?.
|
|
166
|
+
const inbox = ctx.completionInbox?.holdsTurn ? ctx.completionInbox : undefined
|
|
167
167
|
const jobs = ctx.awaitedJobs?.hasPendingWork ? ctx.awaitedJobs : undefined
|
|
168
168
|
if (!inbox && !jobs) return false
|
|
169
169
|
|
|
@@ -370,7 +370,14 @@ export function settleOutstandingWork(ctx: IterationContext): void {
|
|
|
370
370
|
/** Work this turn walked away from. See {@link settleOutstandingWork}. */
|
|
371
371
|
export function recordAbandonedWork(ctx: IterationContext): void {
|
|
372
372
|
const abandoned = ctx.completionInbox?.outstandingTaskIds ?? []
|
|
373
|
-
if (abandoned.length > 0) {
|
|
373
|
+
if (abandoned.length > 0 && ctx.completionInbox?.holdsTurnOpen === false) {
|
|
374
|
+
// Handed over on purpose: the host keeps these running and tells the
|
|
375
|
+
// model when they end, so the turn did not walk away from them.
|
|
376
|
+
ctx.log.info('Turn ended with delegated work left running in the background', {
|
|
377
|
+
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
378
|
+
'namzu.runtime.tasks': abandoned,
|
|
379
|
+
})
|
|
380
|
+
} else if (abandoned.length > 0) {
|
|
374
381
|
ctx.log.warn('Turn ended with delegated work still running', {
|
|
375
382
|
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
376
383
|
'namzu.runtime.tasks': abandoned,
|
|
@@ -30,8 +30,8 @@ export const COMPACTION_REFILL_LIMIT = 3
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* One short line, shown once, when the turn stops on refills. It names the
|
|
33
|
-
* cause and a step that keeps the conversation:
|
|
34
|
-
*
|
|
33
|
+
* cause and a step that keeps the conversation: starting over is never
|
|
34
|
+
* offered here.
|
|
35
35
|
*/
|
|
36
36
|
export const COMPACTION_REFILL_MESSAGE =
|
|
37
37
|
'The context filled up again right after being compacted, three times in a row. A file being read or a tool output is probably too large for the context window. Try reading it in smaller parts.'
|
|
@@ -221,8 +221,7 @@ export function measureContext(ctx: IterationContext): {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
* Reactive compactions allowed for one overflow,
|
|
225
|
-
* terminal: the first relief, then at most one deeper pass.
|
|
224
|
+
* Reactive compactions allowed for one overflow: the first relief, then at most one deeper pass.
|
|
226
225
|
*/
|
|
227
226
|
export const OVERFLOW_RELIEF_LIMIT = 2
|
|
228
227
|
|
|
@@ -350,8 +349,8 @@ export async function relieveOverflow(
|
|
|
350
349
|
|
|
351
350
|
/**
|
|
352
351
|
* Why the last compaction attempt errored, kept for the turn so that when
|
|
353
|
-
* relief runs out the failure it ends on can say so,
|
|
354
|
-
*
|
|
352
|
+
* relief runs out the failure it ends on can say so, and it reads
|
|
353
|
+
* " · automatic compaction failed: <cause>".
|
|
355
354
|
*/
|
|
356
355
|
const compactionErrors = new WeakMap<object, string>()
|
|
357
356
|
const MAX_COMPACTION_CAUSE_CHARS = 300
|
|
@@ -430,9 +429,8 @@ async function declined(
|
|
|
430
429
|
/**
|
|
431
430
|
* The third refill in a row: stop the turn instead of compacting again.
|
|
432
431
|
* Returns true when this pass must not run. Only with `circuitBreaker`.
|
|
433
|
-
* The loop also asks it before the first reactive relief of an overflow,
|
|
434
|
-
*
|
|
435
|
-
* handling), so a re-read loop made only of overflows is counted too.
|
|
432
|
+
* The loop also asks it before the first reactive relief of an overflow, so a re-read loop made
|
|
433
|
+
* only of overflows is counted too.
|
|
436
434
|
*/
|
|
437
435
|
export async function stopOnRefill(ctx: IterationContext): Promise<boolean> {
|
|
438
436
|
const breaker = compactionBreaker(ctx)
|
|
@@ -721,7 +719,7 @@ async function runCompactionCheckInner(
|
|
|
721
719
|
const breaker = compactionBreaker(ctx)
|
|
722
720
|
if (breaker?.failuresOpen && !options?.force) {
|
|
723
721
|
if (breaker.takeFailureNotice()) {
|
|
724
|
-
// Silent for the person
|
|
722
|
+
// Silent for the person: a log line and the
|
|
725
723
|
// event for a host that wants it, but no sentence to show. Only the
|
|
726
724
|
// proactive pass stops; a real overflow still runs the reactive ladder.
|
|
727
725
|
ctx.log.warn('Compaction failed three times in a row — switched off for this turn', {
|
|
@@ -21,7 +21,7 @@ import { isProviderRejectedImageError } from './provider-rejected-image.js'
|
|
|
21
21
|
* Repair-and-retry for a request a provider rejects as invalid (a 400 that is
|
|
22
22
|
* not a context overflow), so a conversation does not dead-end on it.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
24
|
+
* The silent 400 recoveries, in order: pair tool
|
|
25
25
|
* calls with results before a send, strip rejected reasoning, drop a
|
|
26
26
|
* parameter the model refuses, and take rejected media out of the OUTGOING
|
|
27
27
|
* request. Stored history is never edited.
|
|
@@ -77,7 +77,7 @@ type Rejection =
|
|
|
77
77
|
/** Most repair resends for one original request, across every send it makes. */
|
|
78
78
|
export const MAX_REPAIRS_PER_REQUEST = 5
|
|
79
79
|
const MAX_STRIPS_PER_INDEX = 3
|
|
80
|
-
/**
|
|
80
|
+
/** Media is dropped in these steps for an unprocessable request. */
|
|
81
81
|
const GENERIC_MEDIA_LIMITS: readonly number[] = [510, 255, 127]
|
|
82
82
|
|
|
83
83
|
const PARAMETER_CODES: ReadonlySet<string> = new Set([
|
|
@@ -608,7 +608,7 @@ export class RequestRepair {
|
|
|
608
608
|
victims = slots.filter((slot) => slot.messageIndex === newest.messageIndex)
|
|
609
609
|
}
|
|
610
610
|
} else {
|
|
611
|
-
// The
|
|
611
|
+
// The media steps, then halving for smaller requests, down to
|
|
612
612
|
// none: each step keeps fewer than the request held, newest first.
|
|
613
613
|
const limit =
|
|
614
614
|
GENERIC_MEDIA_LIMITS.find((candidate) => candidate < slots.length) ??
|
|
@@ -46,8 +46,7 @@ export interface PathEscalationContext {
|
|
|
46
46
|
* a root that itself sits under a protected name is not a reason to ask about
|
|
47
47
|
* every file in it. The case that matters is the managed worktree a sub-agent
|
|
48
48
|
* runs in, `~/.namzu/projects/<slug>/worktrees/<name>`, which lies under
|
|
49
|
-
* `.namzu` by construction
|
|
50
|
-
* for its own worktrees directory. A path in no root is looked at whole.
|
|
49
|
+
* `.namzu` by construction. A path in no root is looked at whole.
|
|
51
50
|
*/
|
|
52
51
|
function protectedScope(absolute: string, roots: readonly string[]): string {
|
|
53
52
|
let best: string | undefined
|
|
@@ -194,7 +193,7 @@ export async function shellEscalations(
|
|
|
194
193
|
if (!context.reviewOutside) continue
|
|
195
194
|
if (unresolved) {
|
|
196
195
|
// A write whose target is decided at runtime cannot be checked; a
|
|
197
|
-
// read of one is left
|
|
196
|
+
// read of one is left alone.
|
|
198
197
|
if (use.access === 'write') outside.add(spelled)
|
|
199
198
|
continue
|
|
200
199
|
}
|
|
@@ -43,6 +43,11 @@ const UNOWNED_BUFFER_LIMIT = 32
|
|
|
43
43
|
*/
|
|
44
44
|
const OWNED_WORK_DISPLAY_LIMIT = 16
|
|
45
45
|
|
|
46
|
+
export interface CompletionInboxOptions {
|
|
47
|
+
/** See {@link CompletionInbox.holdsTurnOpen}. Default `true`. */
|
|
48
|
+
readonly holdsTurnOpen?: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
/**
|
|
47
52
|
* Completions that finished with nobody left to hear them.
|
|
48
53
|
*
|
|
@@ -151,7 +156,23 @@ export class CompletionInbox {
|
|
|
151
156
|
* `log` is optional and unresolved until it is actually needed (the
|
|
152
157
|
* eviction-warning path in `hold()`), same reason as `LocalTaskScheduler`.
|
|
153
158
|
*/
|
|
154
|
-
constructor(
|
|
159
|
+
constructor(
|
|
160
|
+
private readonly log?: Logger,
|
|
161
|
+
options: CompletionInboxOptions = {},
|
|
162
|
+
) {
|
|
163
|
+
this.holdsTurnOpen = options.holdsTurnOpen !== false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether a turn about to end waits here for the tasks still running.
|
|
168
|
+
*
|
|
169
|
+
* `true` (the default) is the original contract: the turn parks at its settle
|
|
170
|
+
* point until the tasks it launched report back, so their results enter the
|
|
171
|
+
* same turn. `false` lets the turn end with them still running; the host owns
|
|
172
|
+
* their completion from then on (it keeps them alive and wakes the model when
|
|
173
|
+
* they end). Completions that have already arrived are delivered either way.
|
|
174
|
+
*/
|
|
175
|
+
readonly holdsTurnOpen: boolean
|
|
155
176
|
|
|
156
177
|
/**
|
|
157
178
|
* Start listening.
|
|
@@ -349,6 +370,15 @@ export class CompletionInbox {
|
|
|
349
370
|
return this.unheard.size > 0 || this.outstanding.size > 0
|
|
350
371
|
}
|
|
351
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Whether the turn has a reason to wait at its settle point: anything
|
|
375
|
+
* pending when it {@link holdsTurnOpen}, else only a completion already in
|
|
376
|
+
* hand (waiting for its persistence gate).
|
|
377
|
+
*/
|
|
378
|
+
get holdsTurn(): boolean {
|
|
379
|
+
return this.holdsTurnOpen ? this.hasPendingWork : this.unheard.size > 0
|
|
380
|
+
}
|
|
381
|
+
|
|
352
382
|
/**
|
|
353
383
|
* Wait for the next completion, deadline or abort, whichever comes first.
|
|
354
384
|
* Aborting releases only this waiter; work and undelivered results remain owned.
|
|
@@ -22,7 +22,7 @@ export interface CreateWorkspaceParams {
|
|
|
22
22
|
/**
|
|
23
23
|
* Traceable name below the configured workspace root. Nested names are
|
|
24
24
|
* accepted; absolute paths, traversal, and paths rewritten by a filesystem
|
|
25
|
-
* link are refused by the
|
|
25
|
+
* link are refused by the Git driver.
|
|
26
26
|
*/
|
|
27
27
|
label?: string
|
|
28
28
|
}
|
|
@@ -26,7 +26,7 @@ export interface InMemorySessionCheckpointStoreOptions {
|
|
|
26
26
|
*
|
|
27
27
|
* It keeps the serialised bytes rather than the object, so `restore` hashes
|
|
28
28
|
* exactly what `write` hashed and refuses exactly what the disk store
|
|
29
|
-
* refuses. It is the
|
|
29
|
+
* refuses. It is the counterpart of the disk store and the store an
|
|
30
30
|
* in-memory session keeps its checkpoints in.
|
|
31
31
|
*/
|
|
32
32
|
export class InMemorySessionCheckpointStore implements SessionCheckpointStore {
|
|
@@ -249,7 +249,7 @@ export interface MemoryFileFields {
|
|
|
249
249
|
/**
|
|
250
250
|
* The file for one memory. Fields that repeat another are left out — a title
|
|
251
251
|
* equal to the name, a summary equal to the description, the default format —
|
|
252
|
-
* so an ordinary file carries the
|
|
252
|
+
* so an ordinary file carries the canonical shape and nothing else.
|
|
253
253
|
*/
|
|
254
254
|
export function formatMemoryFile(fields: MemoryFileFields, body: string): string {
|
|
255
255
|
const lines: string[] = ['---']
|