@vellumai/assistant 0.5.5 → 0.5.6
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/Dockerfile +3 -4
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +113 -0
- package/src/__tests__/config-schema.test.ts +2 -2
- package/src/__tests__/context-window-manager.test.ts +78 -0
- package/src/__tests__/conversation-title-service.test.ts +30 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
- package/src/__tests__/memory-regressions.test.ts +8 -30
- package/src/__tests__/require-fresh-approval.test.ts +4 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -0
- package/src/cli/commands/conversations.ts +0 -18
- package/src/config/env.ts +8 -2
- package/src/config/feature-flag-registry.json +0 -8
- package/src/config/schema.ts +0 -12
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/platform.ts +1 -1
- package/src/config/schemas/security.ts +4 -0
- package/src/context/window-manager.ts +53 -2
- package/src/daemon/config-watcher.ts +1 -4
- package/src/daemon/conversation-agent-loop.ts +0 -60
- package/src/daemon/conversation-memory.ts +0 -117
- package/src/daemon/conversation-runtime-assembly.ts +0 -2
- package/src/daemon/handlers/conversations.ts +0 -11
- package/src/daemon/lifecycle.ts +3 -46
- package/src/followups/followup-store.ts +5 -2
- package/src/memory/conversation-crud.ts +0 -236
- package/src/memory/conversation-title-service.ts +26 -10
- package/src/memory/db-init.ts +5 -13
- package/src/memory/indexer.ts +15 -106
- package/src/memory/job-handlers/embedding.ts +0 -79
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +0 -8
- package/src/memory/jobs-worker.ts +0 -20
- package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
- package/src/memory/migrations/index.ts +1 -3
- package/src/memory/qdrant-client.ts +4 -6
- package/src/memory/schema/conversations.ts +0 -3
- package/src/memory/schema/index.ts +0 -2
- package/src/messaging/draft-store.ts +2 -2
- package/src/permissions/defaults.ts +3 -3
- package/src/permissions/trust-client.ts +2 -13
- package/src/permissions/trust-store.ts +8 -3
- package/src/runtime/auth/route-policy.ts +14 -0
- package/src/runtime/auth/token-service.ts +133 -0
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/routes/conversation-management-routes.ts +0 -36
- package/src/runtime/routes/conversation-query-routes.ts +44 -2
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/memory-item-routes.test.ts +221 -3
- package/src/runtime/routes/memory-item-routes.ts +124 -2
- package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
- package/src/schedule/schedule-store.ts +0 -21
- package/src/skills/inline-command-render.ts +5 -1
- package/src/skills/inline-command-runner.ts +30 -2
- package/src/tools/memory/handlers.ts +1 -129
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/skills/load.ts +9 -2
- package/src/util/platform.ts +5 -5
- package/src/util/xml.ts +8 -0
- package/src/workspace/heartbeat-service.ts +5 -24
- package/src/__tests__/archive-recall.test.ts +0 -560
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
- package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
- package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
- package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
- package/src/__tests__/memory-brief-time.test.ts +0 -285
- package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
- package/src/__tests__/memory-chunk-archive.test.ts +0 -400
- package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
- package/src/__tests__/memory-episode-archive.test.ts +0 -370
- package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
- package/src/__tests__/memory-observation-archive.test.ts +0 -375
- package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
- package/src/__tests__/memory-reducer-job.test.ts +0 -538
- package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
- package/src/__tests__/memory-reducer-store.test.ts +0 -728
- package/src/__tests__/memory-reducer-types.test.ts +0 -707
- package/src/__tests__/memory-reducer.test.ts +0 -704
- package/src/__tests__/memory-simplified-config.test.ts +0 -281
- package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
- package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
- package/src/config/schemas/memory-simplified.ts +0 -101
- package/src/memory/archive-recall.ts +0 -516
- package/src/memory/archive-store.ts +0 -400
- package/src/memory/brief-formatting.ts +0 -33
- package/src/memory/brief-open-loops.ts +0 -266
- package/src/memory/brief-time.ts +0 -162
- package/src/memory/brief.ts +0 -75
- package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
- package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
- package/src/memory/migrations/185-memory-brief-state.ts +0 -52
- package/src/memory/migrations/186-memory-archive.ts +0 -109
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
- package/src/memory/reducer-scheduler.ts +0 -242
- package/src/memory/reducer-store.ts +0 -271
- package/src/memory/reducer-types.ts +0 -106
- package/src/memory/reducer.ts +0 -467
- package/src/memory/schema/memory-archive.ts +0 -121
- package/src/memory/schema/memory-brief.ts +0 -55
|
@@ -206,27 +206,6 @@ export function listSchedules(options?: {
|
|
|
206
206
|
return rows.map(parseJobRow);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
/**
|
|
210
|
-
* Return enabled schedules whose next run falls within a time window.
|
|
211
|
-
* Used by the memory brief compiler to surface due-soon schedule entries.
|
|
212
|
-
*/
|
|
213
|
-
export function getDueSoonSchedules(
|
|
214
|
-
now: number,
|
|
215
|
-
horizonMs: number,
|
|
216
|
-
): ScheduleJob[] {
|
|
217
|
-
const db = getDb();
|
|
218
|
-
const cutoff = now + horizonMs;
|
|
219
|
-
const rows = db
|
|
220
|
-
.select()
|
|
221
|
-
.from(scheduleJobs)
|
|
222
|
-
.where(
|
|
223
|
-
and(eq(scheduleJobs.enabled, true), lte(scheduleJobs.nextRunAt, cutoff)),
|
|
224
|
-
)
|
|
225
|
-
.orderBy(asc(scheduleJobs.nextRunAt))
|
|
226
|
-
.all();
|
|
227
|
-
return rows.map(parseJobRow);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
209
|
export function updateSchedule(
|
|
231
210
|
id: string,
|
|
232
211
|
updates: {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { getLogger } from "../util/logger.js";
|
|
17
|
+
import { escapeXmlContent } from "../util/xml.js";
|
|
17
18
|
import type { InlineCommandExpansion } from "./inline-command-expansions.js";
|
|
18
19
|
import type { InlineCommandResult } from "./inline-command-runner.js";
|
|
19
20
|
import { runInlineCommand } from "./inline-command-runner.js";
|
|
@@ -91,7 +92,10 @@ export async function renderInlineCommands(
|
|
|
91
92
|
|
|
92
93
|
let replacement: string;
|
|
93
94
|
if (commandResult.ok) {
|
|
94
|
-
replacement = wrapInXml(
|
|
95
|
+
replacement = wrapInXml(
|
|
96
|
+
expansion.placeholderId,
|
|
97
|
+
escapeXmlContent(commandResult.output),
|
|
98
|
+
);
|
|
95
99
|
expandedCount++;
|
|
96
100
|
} else {
|
|
97
101
|
const stub = failureReasonToStub(commandResult);
|
|
@@ -37,6 +37,15 @@ const DEFAULT_TIMEOUT_MS = 10_000;
|
|
|
37
37
|
/** Maximum output characters before truncation. */
|
|
38
38
|
const MAX_OUTPUT_CHARS = 20_000;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Maximum bytes to buffer from stdout during streaming. Once this limit is
|
|
42
|
+
* reached we stop accepting data so a long-running command (e.g. `yes`) cannot
|
|
43
|
+
* grow memory unbounded before the timeout fires. Set generously above
|
|
44
|
+
* MAX_OUTPUT_CHARS to account for multi-byte UTF-8 and ANSI sequences that will
|
|
45
|
+
* be stripped before the character-level clamp.
|
|
46
|
+
*/
|
|
47
|
+
const MAX_STDOUT_BUFFER_BYTES = MAX_OUTPUT_CHARS * 4;
|
|
48
|
+
|
|
40
49
|
/**
|
|
41
50
|
* ANSI escape sequence pattern (covers SGR, cursor movement, erase, etc.).
|
|
42
51
|
* Matches: ESC[ ... final_byte and ESC] ... ST (OSC sequences).
|
|
@@ -115,7 +124,9 @@ export async function runInlineCommand(
|
|
|
115
124
|
|
|
116
125
|
return new Promise<InlineCommandResult>((resolve) => {
|
|
117
126
|
let timedOut = false;
|
|
127
|
+
let stdoutCapped = false;
|
|
118
128
|
const stdoutChunks: Buffer[] = [];
|
|
129
|
+
let stdoutBytes = 0;
|
|
119
130
|
|
|
120
131
|
let child: ReturnType<typeof spawn>;
|
|
121
132
|
try {
|
|
@@ -140,7 +151,19 @@ export async function runInlineCommand(
|
|
|
140
151
|
child.kill("SIGKILL");
|
|
141
152
|
}, timeoutMs);
|
|
142
153
|
|
|
143
|
-
child.stdout!.on("data", (data: Buffer) =>
|
|
154
|
+
child.stdout!.on("data", (data: Buffer) => {
|
|
155
|
+
if (stdoutBytes >= MAX_STDOUT_BUFFER_BYTES) return;
|
|
156
|
+
stdoutChunks.push(data);
|
|
157
|
+
stdoutBytes += data.length;
|
|
158
|
+
if (stdoutBytes >= MAX_STDOUT_BUFFER_BYTES) {
|
|
159
|
+
// Stop reading to release backpressure on the child process.
|
|
160
|
+
// This destroys the read end of the pipe, which may cause the
|
|
161
|
+
// child to receive SIGPIPE and exit with code=null. The
|
|
162
|
+
// stdoutCapped flag lets the close handler treat this as success.
|
|
163
|
+
stdoutCapped = true;
|
|
164
|
+
child.stdout!.destroy();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
144
167
|
|
|
145
168
|
child.on("close", (code) => {
|
|
146
169
|
clearTimeout(timer);
|
|
@@ -157,7 +180,12 @@ export async function runInlineCommand(
|
|
|
157
180
|
}
|
|
158
181
|
|
|
159
182
|
// ── Non-zero exit ────────────────────────────────────────────────
|
|
160
|
-
|
|
183
|
+
// When stdout was capped we destroyed the read end of the pipe,
|
|
184
|
+
// which typically causes SIGPIPE — the process is killed by the
|
|
185
|
+
// signal so the exit code is null. Only suppress the error in that
|
|
186
|
+
// specific case; a command that outputs a lot but exits with a
|
|
187
|
+
// genuine non-zero code (e.g. exit 1) should still be an error.
|
|
188
|
+
if (code !== 0 && !(stdoutCapped && code == null)) {
|
|
161
189
|
log.debug(
|
|
162
190
|
{ command, exitCode: code },
|
|
163
191
|
"Inline command exited with non-zero code",
|
|
@@ -2,8 +2,6 @@ import { and, eq, ne } from "drizzle-orm";
|
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
|
|
4
4
|
import type { AssistantConfig } from "../../config/types.js";
|
|
5
|
-
import { buildArchiveRecall } from "../../memory/archive-recall.js";
|
|
6
|
-
import { insertObservation } from "../../memory/archive-store.js";
|
|
7
5
|
import { getDb } from "../../memory/db.js";
|
|
8
6
|
import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
|
|
9
7
|
import { enqueueMemoryJob } from "../../memory/jobs-store.js";
|
|
@@ -20,7 +18,7 @@ const log = getLogger("memory-tools");
|
|
|
20
18
|
|
|
21
19
|
export async function handleMemorySave(
|
|
22
20
|
args: Record<string, unknown>,
|
|
23
|
-
|
|
21
|
+
_config: AssistantConfig,
|
|
24
22
|
conversationId: string,
|
|
25
23
|
messageId: string | undefined,
|
|
26
24
|
scopeId: string = "default",
|
|
@@ -65,19 +63,6 @@ export async function handleMemorySave(
|
|
|
65
63
|
? truncate(args.subject.trim(), 80, "")
|
|
66
64
|
: inferSubjectFromStatement(statement.trim());
|
|
67
65
|
|
|
68
|
-
// When simplified memory is enabled, save directly to the simplified
|
|
69
|
-
// observation/chunk tables instead of the legacy memory_items table.
|
|
70
|
-
if (config.memory.simplified.enabled) {
|
|
71
|
-
return handleSimplifiedMemorySave(
|
|
72
|
-
kind,
|
|
73
|
-
subject,
|
|
74
|
-
statement.trim(),
|
|
75
|
-
conversationId,
|
|
76
|
-
messageId,
|
|
77
|
-
scopeId,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
66
|
try {
|
|
82
67
|
const db = getDb();
|
|
83
68
|
const id = uuid();
|
|
@@ -290,12 +275,6 @@ export async function handleMemoryRecall(
|
|
|
290
275
|
? args.scope.trim()
|
|
291
276
|
: "default";
|
|
292
277
|
|
|
293
|
-
// When simplified memory is enabled, use the archive recall path
|
|
294
|
-
// instead of the legacy hybrid retriever.
|
|
295
|
-
if (config.memory.simplified.enabled) {
|
|
296
|
-
return handleSimplifiedMemoryRecall(query.trim(), scopeId ?? "default");
|
|
297
|
-
}
|
|
298
|
-
|
|
299
278
|
// Scope policy: "conversation" means strict (only that scope),
|
|
300
279
|
// anything else allows fallback to the default scope.
|
|
301
280
|
const scopePolicyOverride: ScopePolicyOverride | undefined = scopeId
|
|
@@ -432,113 +411,6 @@ export async function handleMemoryDelete(
|
|
|
432
411
|
}
|
|
433
412
|
}
|
|
434
413
|
|
|
435
|
-
// ── Simplified memory helpers ────────────────────────────────────────
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Save a memory item as an observation + chunk in the simplified system.
|
|
439
|
-
* This is used when simplified memory is enabled instead of writing to
|
|
440
|
-
* the legacy memory_items table.
|
|
441
|
-
*/
|
|
442
|
-
function handleSimplifiedMemorySave(
|
|
443
|
-
kind: string,
|
|
444
|
-
subject: string,
|
|
445
|
-
statement: string,
|
|
446
|
-
conversationId: string,
|
|
447
|
-
messageId: string | undefined,
|
|
448
|
-
scopeId: string,
|
|
449
|
-
): ToolExecutionResult {
|
|
450
|
-
try {
|
|
451
|
-
const trimmedStatement = truncate(statement, 500, "");
|
|
452
|
-
const content = `[${kind}] ${subject}: ${trimmedStatement}`;
|
|
453
|
-
|
|
454
|
-
const result = insertObservation({
|
|
455
|
-
conversationId,
|
|
456
|
-
messageId: messageId ?? null,
|
|
457
|
-
role: "user",
|
|
458
|
-
content,
|
|
459
|
-
scopeId,
|
|
460
|
-
modality: "text",
|
|
461
|
-
source: "tool:memory_save",
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
log.debug(
|
|
465
|
-
{
|
|
466
|
-
observationId: result.observationId,
|
|
467
|
-
chunkId: result.chunkId,
|
|
468
|
-
kind,
|
|
469
|
-
subject,
|
|
470
|
-
conversationId,
|
|
471
|
-
messageId,
|
|
472
|
-
},
|
|
473
|
-
"Memory saved via simplified system",
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
return {
|
|
477
|
-
content: `Saved to memory (ID: ${result.observationId}).\nKind: ${kind}\nSubject: ${subject}\nStatement: ${trimmedStatement}`,
|
|
478
|
-
isError: false,
|
|
479
|
-
};
|
|
480
|
-
} catch (err) {
|
|
481
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
482
|
-
log.error({ err }, "simplified memory_save failed");
|
|
483
|
-
return { content: `Error: Failed to save memory: ${msg}`, isError: true };
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Recall memories using the simplified archive recall path instead of
|
|
489
|
-
* the legacy hybrid retriever.
|
|
490
|
-
*/
|
|
491
|
-
function handleSimplifiedMemoryRecall(
|
|
492
|
-
query: string,
|
|
493
|
-
scopeId: string,
|
|
494
|
-
): ToolExecutionResult {
|
|
495
|
-
try {
|
|
496
|
-
const recallResult = buildArchiveRecall(scopeId, query);
|
|
497
|
-
|
|
498
|
-
if (recallResult.bullets.length === 0) {
|
|
499
|
-
return {
|
|
500
|
-
content: JSON.stringify({
|
|
501
|
-
text: "No matching memories found.",
|
|
502
|
-
resultCount: 0,
|
|
503
|
-
degraded: false,
|
|
504
|
-
items: [],
|
|
505
|
-
sources: { semantic: 0, recency: 0 },
|
|
506
|
-
}),
|
|
507
|
-
isError: false,
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const items = recallResult.bullets.map((b) => ({
|
|
512
|
-
id: b.sourceId,
|
|
513
|
-
type: b.source,
|
|
514
|
-
kind: b.source,
|
|
515
|
-
}));
|
|
516
|
-
|
|
517
|
-
const result = {
|
|
518
|
-
text: recallResult.text,
|
|
519
|
-
resultCount: recallResult.bullets.length,
|
|
520
|
-
degraded: false,
|
|
521
|
-
items,
|
|
522
|
-
sources: {
|
|
523
|
-
semantic: recallResult.prefetchHitCount,
|
|
524
|
-
recency: 0,
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
return {
|
|
529
|
-
content: JSON.stringify(result),
|
|
530
|
-
isError: false,
|
|
531
|
-
};
|
|
532
|
-
} catch (err) {
|
|
533
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
534
|
-
log.error({ err, query }, "simplified memory_recall failed");
|
|
535
|
-
return {
|
|
536
|
-
content: `Error: Memory recall failed: ${msg}`,
|
|
537
|
-
isError: true,
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
414
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
543
415
|
|
|
544
416
|
function inferSubjectFromStatement(statement: string): string {
|
|
@@ -137,6 +137,24 @@ export class PermissionChecker {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
if (result.decision === "prompt") {
|
|
140
|
+
// dangerouslySkipPermissions: when enabled, auto-approve all prompts
|
|
141
|
+
// without user interaction. Deny rules are still respected (they
|
|
142
|
+
// return before reaching this block).
|
|
143
|
+
//
|
|
144
|
+
// Note: unlike guardian auto-approve and temporary overrides below,
|
|
145
|
+
// this intentionally does NOT check `context.requireFreshApproval`.
|
|
146
|
+
// The setting is designed to skip ALL interactive prompts
|
|
147
|
+
// unconditionally — it is an explicit operator opt-out from the
|
|
148
|
+
// permission system, so requireFreshApproval does not apply.
|
|
149
|
+
const cfg = getConfig();
|
|
150
|
+
if (cfg.permissions.dangerouslySkipPermissions) {
|
|
151
|
+
log.info(
|
|
152
|
+
{ toolName: name, riskLevel },
|
|
153
|
+
"dangerouslySkipPermissions active — auto-approving without prompt",
|
|
154
|
+
);
|
|
155
|
+
return { allowed: true, decision: "dangerously_skip_permissions", riskLevel };
|
|
156
|
+
}
|
|
157
|
+
|
|
140
158
|
// Guardian-trust sessions (e.g. scheduled jobs, reminders) should be
|
|
141
159
|
// able to use bundled tools without interactive approval. The guardian
|
|
142
160
|
// is the owner - prompting makes no sense when there is no client.
|
package/src/tools/skills/load.ts
CHANGED
|
@@ -440,9 +440,16 @@ export class SkillLoadTool implements Tool {
|
|
|
440
440
|
"Rendered inline command expansions for included skill",
|
|
441
441
|
);
|
|
442
442
|
} catch (err) {
|
|
443
|
-
log.
|
|
443
|
+
log.error(
|
|
444
444
|
{ err, skillId: childId, parentSkillId: skill.id },
|
|
445
|
-
"Failed to render inline commands for included skill
|
|
445
|
+
"Failed to render inline commands for included skill; falling back to sanitized body",
|
|
446
|
+
);
|
|
447
|
+
// Strip raw !`...` inline command tokens so they don't leak into
|
|
448
|
+
// the prompt. Replace with a safe stub to maintain fail-closed
|
|
449
|
+
// contract for raw tokens while still isolating child failures.
|
|
450
|
+
childBody = childBody.replace(
|
|
451
|
+
/!`[^`]*`/g,
|
|
452
|
+
"[inline command unavailable]",
|
|
446
453
|
);
|
|
447
454
|
}
|
|
448
455
|
}
|
package/src/util/platform.ts
CHANGED
|
@@ -436,11 +436,11 @@ export function ensureDataDir(): void {
|
|
|
436
436
|
const dirs = [
|
|
437
437
|
// Root-level dirs (runtime)
|
|
438
438
|
root,
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
439
|
+
// signals dir is needed everywhere (MCP reload, user-message signals)
|
|
440
|
+
join(root, "signals"),
|
|
441
|
+
// protected, hooks are local-only — skip in containerized mode
|
|
442
|
+
// (credentials via CES HTTP API, trust via gateway API)
|
|
443
|
+
...(containerized ? [] : [join(root, "protected"), join(root, "hooks")]),
|
|
444
444
|
// Workspace dirs
|
|
445
445
|
workspace,
|
|
446
446
|
join(workspace, "skills"),
|
package/src/util/xml.ts
CHANGED
|
@@ -6,3 +6,11 @@ export function escapeXmlAttr(s: string): string {
|
|
|
6
6
|
.replace(/</g, "<")
|
|
7
7
|
.replace(/>/g, ">");
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
/** Escape a string for safe inclusion as XML/HTML text content. */
|
|
11
|
+
export function escapeXmlContent(s: string): string {
|
|
12
|
+
return s
|
|
13
|
+
.replace(/&/g, "&")
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/>/g, ">");
|
|
16
|
+
}
|
|
@@ -210,13 +210,11 @@ export class WorkspaceHeartbeatService {
|
|
|
210
210
|
|
|
211
211
|
try {
|
|
212
212
|
const now = this.now();
|
|
213
|
-
let shutdownFiles: string[] = [];
|
|
214
213
|
const { committed } = await service.commitIfDirty(
|
|
215
214
|
(st) => {
|
|
216
215
|
const uniqueFiles = [
|
|
217
216
|
...new Set([...st.staged, ...st.modified, ...st.untracked]),
|
|
218
217
|
];
|
|
219
|
-
shutdownFiles = uniqueFiles;
|
|
220
218
|
log.info(
|
|
221
219
|
{ workspaceDir, totalChanges: uniqueFiles.length },
|
|
222
220
|
"Committing pending changes on shutdown",
|
|
@@ -237,28 +235,11 @@ export class WorkspaceHeartbeatService {
|
|
|
237
235
|
if (committed) {
|
|
238
236
|
firstSeenDirty.delete(workspaceDir);
|
|
239
237
|
result.committed++;
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
workspaceDir,
|
|
246
|
-
trigger: "shutdown",
|
|
247
|
-
changedFiles: shutdownFiles,
|
|
248
|
-
timestampMs: this.now(),
|
|
249
|
-
};
|
|
250
|
-
getEnrichmentService().enqueue({
|
|
251
|
-
workspaceDir,
|
|
252
|
-
commitHash,
|
|
253
|
-
context: shutdownCtx,
|
|
254
|
-
gitService: service,
|
|
255
|
-
});
|
|
256
|
-
} catch (enrichErr) {
|
|
257
|
-
log.debug(
|
|
258
|
-
{ enrichErr },
|
|
259
|
-
"Failed to enqueue shutdown enrichment (non-fatal)",
|
|
260
|
-
);
|
|
261
|
-
}
|
|
238
|
+
// Skip enrichment for shutdown commits — the enrichment queue is
|
|
239
|
+
// about to be shut down anyway, and the fire-and-forget writeNote()
|
|
240
|
+
// can race with subsequent commitAllPending() calls (the async
|
|
241
|
+
// git-notes operation acquires the mutex and may leave behind an
|
|
242
|
+
// index.lock on some git versions, causing the next commit to fail).
|
|
262
243
|
} else {
|
|
263
244
|
result.skipped++;
|
|
264
245
|
}
|