Skip to content

Commit b7b7334

Browse files
committed
Add initial Kato workspace configuration file
- Created a new YAML configuration file for Kato workspace. - Defined workspace ID, output directory, and filename template. - Configured markdown frontmatter options including participant username and event kinds. - Enabled feature flags for commentary, thinking, and tool calls in the writer.
1 parent e56b1a9 commit b7b7334

16 files changed

Lines changed: 1990 additions & 85 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ Rules:
114114

115115
- `::record`, `::capture`, and `::export` require a workspace alias suffix.
116116
- `::init` / `::init-<alias>` are unsupported and treated as invalid commands.
117+
- `::capture-<alias>` writes a snapshot and activates recording for that
118+
workspace output (current binding when pathless, resolved target when path is
119+
provided).
117120
- `::stop` stops all active workspace outputs for the session.
118121
- `::stop-<alias>` stops only the active output bound to that alias.
119122
- Explicit path arguments may be absolute or relative, and may point to a file
@@ -265,6 +268,12 @@ Supported `filenameTemplate` tokens:
265268
- `{provider}`: provider slug (for example `codex`)
266269
- `{sessionId}`: full session id slug
267270
- `{sessionShortId}`: first 8 chars of session id (slugged)
271+
- `{YYYY}`: 4-digit year in `filenameTemplateTimezone`
272+
- `{YY}`: 2-digit year in `filenameTemplateTimezone`
273+
- `{MM}`: 2-digit month in `filenameTemplateTimezone`
274+
- `{DD}`: 2-digit day in `filenameTemplateTimezone`
275+
- `{HH}`: 24-hour clock hour in `filenameTemplateTimezone`
276+
- `{mm}`: 2-digit minute in `filenameTemplateTimezone`
268277
- `{timestampHumane}`: `YYYY-MM-DD_HHmm` in `filenameTemplateTimezone`
269278
- `{snippetSlug}`: slugified session snippet (`snapshot.metadata.snippet` first,
270279
then command-time snippet extraction, then `conversation`)

apps/daemon/src/cli/commands/status.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,14 @@ function renderSessionRow(
264264

265265
const started = formatRelativeTime(recording.startedAt, now);
266266
const lastWrite = formatRelativeTime(recording.lastWriteAt, now);
267+
const workspaceLabel = recording.workspaceAlias?.trim();
268+
const recordingDetail = workspaceLabel && workspaceLabel.length > 0
269+
? `started ${started} · last write ${lastWrite} · workspace: ${workspaceLabel}`
270+
: `started ${started} · last write ${lastWrite}`;
267271
lines.push(
268272
formatPrefixedLine(
269273
" ",
270-
`started ${started} · last write ${lastWrite}`,
274+
recordingDetail,
271275
width,
272276
),
273277
);

apps/daemon/src/orchestrator/daemon_runtime.ts

Lines changed: 127 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ interface SessionEventProcessingState {
168168
string,
169169
{
170170
workspaceId: string;
171+
workspaceAlias?: string;
171172
currentResolvedPath: string;
172173
desiredState: boolean;
173174
recordingCycleId?: string;
@@ -365,10 +366,18 @@ function readDatePart(
365366
return parts.find((part) => part.type === type)?.value ?? "";
366367
}
367368

368-
function formatTimestampHumane(
369+
function readTimestampTemplateParts(
369370
now: Date,
370371
timeZone: string,
371-
): string {
372+
): {
373+
YYYY: string;
374+
YY: string;
375+
MM: string;
376+
DD: string;
377+
HH: string;
378+
mm: string;
379+
timestampHumane: string;
380+
} {
372381
const formatter = new Intl.DateTimeFormat("en-CA", {
373382
year: "numeric",
374383
month: "2-digit",
@@ -384,7 +393,15 @@ function formatTimestampHumane(
384393
const day = readDatePart(parts, "day");
385394
const hour = readDatePart(parts, "hour");
386395
const minute = readDatePart(parts, "minute");
387-
return `${year}-${month}-${day}_${hour}${minute}`;
396+
return {
397+
YYYY: year,
398+
YY: year.slice(-2),
399+
MM: month,
400+
DD: day,
401+
HH: hour,
402+
mm: minute,
403+
timestampHumane: `${year}-${month}-${day}_${hour}${minute}`,
404+
};
388405
}
389406

390407
function readWorkspaceOutputs(
@@ -523,14 +540,21 @@ function renderWorkspaceFilename(
523540
boundarySnapshot?: ConversationEvent[];
524541
} = {},
525542
): string {
543+
const timestampTokens = readTimestampTemplateParts(
544+
now,
545+
profile.filenameTemplateTimezone,
546+
);
526547
const tokens: Record<string, string> = {
527548
provider: sanitizeFilenamePart(provider),
528549
sessionId: sanitizeFilenamePart(sessionId),
529550
sessionShortId: sanitizeFilenamePart(sessionId.slice(0, 8)),
530-
timestampHumane: formatTimestampHumane(
531-
now,
532-
profile.filenameTemplateTimezone,
533-
),
551+
YYYY: timestampTokens.YYYY,
552+
YY: timestampTokens.YY,
553+
MM: timestampTokens.MM,
554+
DD: timestampTokens.DD,
555+
HH: timestampTokens.HH,
556+
mm: timestampTokens.mm,
557+
timestampHumane: timestampTokens.timestampHumane,
534558
snippetSlug: slugifySnippetForFilename(resolveFilenameSnippet(options)),
535559
};
536560
let rendered = profile.filenameTemplate;
@@ -1240,6 +1264,11 @@ async function applyPersistentControlCommandsForEvent(
12401264
}
12411265
} else if (command.verb === "capture") {
12421266
let targetPath: string;
1267+
let resolvedBinding:
1268+
| NonNullable<SessionMetadataV1["workspaceOutputs"]>[number][
1269+
"currentDestination"
1270+
]
1271+
| undefined;
12431272
let stateChanged = false;
12441273
if (!command.argument && output) {
12451274
targetPath = output.currentResolvedPath;
@@ -1253,14 +1282,15 @@ async function applyPersistentControlCommandsForEvent(
12531282
rawArgument: command.argument,
12541283
now: now(),
12551284
});
1285+
resolvedBinding = resolved.binding;
12561286
targetPath = await validateDestinationPathForCommand(
12571287
recordingPipeline,
12581288
provider,
12591289
providerSessionId,
12601290
resolved.resolvedPath,
12611291
"capture",
12621292
);
1263-
if (!command.argument && !output) {
1293+
if (!output) {
12641294
output = createWorkspaceOutputState({
12651295
profile,
12661296
binding: resolved.binding,
@@ -1271,8 +1301,26 @@ async function applyPersistentControlCommandsForEvent(
12711301
});
12721302
readWorkspaceOutputs(metadata).push(output);
12731303
stateChanged = true;
1304+
} else if (output.currentResolvedPath !== targetPath) {
1305+
closeWorkspaceOutputCycle(
1306+
output,
1307+
writeCursor,
1308+
now().toISOString(),
1309+
);
1310+
applyWorkspaceProfileSnapshot(output, profile);
1311+
if (resolvedBinding) {
1312+
output.currentDestination = resolvedBinding;
1313+
}
1314+
output.currentResolvedPath = targetPath;
1315+
output.writeCursor = writeCursor;
1316+
output.desiredState = "off";
1317+
stateChanged = true;
12741318
}
12751319
}
1320+
if (!output) {
1321+
throw new Error("Workspace output state was not created");
1322+
}
1323+
applyWorkspaceProfileSnapshot(output, profile);
12761324
loggedTargetPath = targetPath;
12771325
const captureEvents = await resolveBoundaryEventsFromTwinStart(
12781326
metadata,
@@ -1285,7 +1333,7 @@ async function applyPersistentControlCommandsForEvent(
12851333
captureEvents,
12861334
providerSessionId,
12871335
);
1288-
const currentCycleId = output?.activeRecordingCycleId;
1336+
const currentCycleId = output.activeRecordingCycleId;
12891337
await recordingPipeline.captureSnapshot({
12901338
provider,
12911339
sessionId: providerSessionId,
@@ -1296,6 +1344,15 @@ async function applyPersistentControlCommandsForEvent(
12961344
workspaceIds: [workspace.workspaceId],
12971345
outputOverrides,
12981346
});
1347+
let activeCycleId = output.activeRecordingCycleId;
1348+
if (!activeCycleId || output.desiredState !== "on") {
1349+
activeCycleId = openWorkspaceOutputCycle(
1350+
output,
1351+
writeCursor,
1352+
now().toISOString(),
1353+
);
1354+
stateChanged = true;
1355+
}
12991356
const continuationEvents = buildCommandSeedEvents(
13001357
event,
13011358
command.line + 1,
@@ -1313,12 +1370,14 @@ async function applyPersistentControlCommandsForEvent(
13131370
targetPath,
13141371
events: continuationEvents,
13151372
title: captureTitle,
1316-
...(currentCycleId ? { recordingId: currentCycleId } : {}),
1317-
recordingCycleIds: currentCycleId ? [currentCycleId] : undefined,
1373+
...(activeCycleId ? { recordingId: activeCycleId } : {}),
1374+
recordingCycleIds: activeCycleId ? [activeCycleId] : undefined,
13181375
workspaceIds: [workspace.workspaceId],
13191376
outputOverrides,
13201377
});
13211378
}
1379+
output.writeCursor = writeCursor;
1380+
stateChanged = true;
13221381
metadataChanged = metadataChanged || stateChanged;
13231382
} else if (command.verb === "export") {
13241383
const resolved = await resolveWorkspaceCommandDestination({
@@ -1580,6 +1639,7 @@ async function applyControlCommandsForEvent(
15801639
loggedTargetPath = resolvedDestination;
15811640
const state = existingState ?? {
15821641
workspaceId: workspace.workspaceId,
1642+
workspaceAlias: profile.alias,
15831643
currentResolvedPath: resolvedDestination,
15841644
desiredState: false,
15851645
outputOverrides,
@@ -1606,6 +1666,7 @@ async function applyControlCommandsForEvent(
16061666
provider,
16071667
sessionId,
16081668
recordingKey: workspace.workspaceId,
1669+
workspaceAlias: profile.alias,
16091670
targetPath: resolvedDestination,
16101671
seedEvents,
16111672
title: recordingTitle,
@@ -1616,6 +1677,7 @@ async function applyControlCommandsForEvent(
16161677
state.currentResolvedPath = resolvedDestination;
16171678
state.desiredState = true;
16181679
state.recordingCycleId = recordingCycleId;
1680+
state.workspaceAlias = profile.alias;
16191681
state.outputOverrides = outputOverrides;
16201682
sessionEventState.workspaceOutputs.set(
16211683
workspace.workspaceId,
@@ -1646,15 +1708,39 @@ async function applyControlCommandsForEvent(
16461708
throw new Error("Unable to resolve workspace capture destination");
16471709
}
16481710
loggedTargetPath = resolvedDestination;
1711+
const state = existingState ?? {
1712+
workspaceId: workspace.workspaceId,
1713+
workspaceAlias: profile.alias,
1714+
currentResolvedPath: resolvedDestination,
1715+
desiredState: false,
1716+
outputOverrides,
1717+
};
1718+
const destinationChanged = state.currentResolvedPath !==
1719+
resolvedDestination;
1720+
if (destinationChanged && state.desiredState) {
1721+
recordingPipeline.stopRecording(
1722+
provider,
1723+
sessionId,
1724+
workspace.workspaceId,
1725+
);
1726+
}
1727+
if (destinationChanged) {
1728+
state.currentResolvedPath = resolvedDestination;
1729+
state.desiredState = false;
1730+
delete state.recordingCycleId;
1731+
}
1732+
state.workspaceAlias = profile.alias;
1733+
state.outputOverrides = outputOverrides;
1734+
const activeCycleId = state.desiredState && state.recordingCycleId
1735+
? state.recordingCycleId
1736+
: undefined;
16491737
await recordingPipeline.captureSnapshot({
16501738
provider,
16511739
sessionId,
16521740
targetPath: resolvedDestination,
16531741
events: boundarySnapshot,
16541742
title: recordingTitle,
1655-
recordingCycleIds: existingState?.recordingCycleId
1656-
? [existingState.recordingCycleId]
1657-
: undefined,
1743+
recordingCycleIds: activeCycleId ? [activeCycleId] : undefined,
16581744
workspaceIds: [workspace.workspaceId],
16591745
outputOverrides,
16601746
});
@@ -1663,36 +1749,37 @@ async function applyControlCommandsForEvent(
16631749
command.line + 1,
16641750
boundary.lastLineInSegment,
16651751
);
1666-
if (continuationEvents.length > 0) {
1667-
if (!recordingPipeline.appendToDestination) {
1668-
throw new Error(
1669-
"Recording pipeline does not support appendToDestination",
1670-
);
1752+
if (state.desiredState && state.recordingCycleId) {
1753+
if (continuationEvents.length > 0) {
1754+
await recordingPipeline.appendToActiveRecording({
1755+
provider,
1756+
sessionId,
1757+
recordingKey: workspace.workspaceId,
1758+
events: continuationEvents,
1759+
title: recordingTitle,
1760+
recordingCycleIds: [state.recordingCycleId],
1761+
workspaceIds: [workspace.workspaceId],
1762+
outputOverrides,
1763+
});
16711764
}
1672-
await recordingPipeline.appendToDestination({
1765+
} else {
1766+
const recordingCycleId = crypto.randomUUID();
1767+
await recordingPipeline.activateRecording({
16731768
provider,
16741769
sessionId,
1770+
recordingKey: workspace.workspaceId,
1771+
workspaceAlias: profile.alias,
16751772
targetPath: resolvedDestination,
1676-
events: continuationEvents,
1773+
seedEvents: continuationEvents,
16771774
title: recordingTitle,
1678-
...(existingState?.recordingCycleId
1679-
? { recordingId: existingState.recordingCycleId }
1680-
: {}),
1681-
recordingCycleIds: existingState?.recordingCycleId
1682-
? [existingState.recordingCycleId]
1683-
: undefined,
1775+
recordingId: recordingCycleId,
16841776
workspaceIds: [workspace.workspaceId],
16851777
outputOverrides,
16861778
});
1779+
state.desiredState = true;
1780+
state.recordingCycleId = recordingCycleId;
16871781
}
1688-
if (!command.argument && !existingState) {
1689-
sessionEventState.workspaceOutputs.set(workspace.workspaceId, {
1690-
workspaceId: workspace.workspaceId,
1691-
currentResolvedPath: resolvedDestination,
1692-
desiredState: false,
1693-
outputOverrides,
1694-
});
1695-
}
1782+
sessionEventState.workspaceOutputs.set(workspace.workspaceId, state);
16961783
} else if (command.verb === "export") {
16971784
const resolved = await resolveWorkspaceCommandDestination({
16981785
profile,
@@ -1858,6 +1945,7 @@ async function processInChatRecordingUpdates(
18581945
destinationRecordingIds: new Map<string, string>(),
18591946
workspaceOutputs: new Map<string, {
18601947
workspaceId: string;
1948+
workspaceAlias?: string;
18611949
currentResolvedPath: string;
18621950
desiredState: boolean;
18631951
recordingCycleId?: string;
@@ -2166,6 +2254,7 @@ function toActiveRecordingsFromMetadata(
21662254
recordingId: output.activeRecordingCycleId ?? output.workspaceId,
21672255
provider: metadata.provider,
21682256
sessionId: metadata.providerSessionId,
2257+
workspaceAlias: output.workspaceAliasSnapshot,
21692258
outputPath: output.currentResolvedPath,
21702259
startedAt: readWorkspaceOutputStartedAt(output) || metadata.updatedAt,
21712260
lastWriteAt: metadata.updatedAt,
@@ -2296,6 +2385,9 @@ function toSessionStatuses(
22962385
...(recording.recordingId
22972386
? { recordingShortId: recording.recordingId.slice(0, 8) }
22982387
: {}),
2388+
...(recording.workspaceAlias
2389+
? { workspaceAlias: recording.workspaceAlias }
2390+
: {}),
22992391
outputPath: recording.outputPath,
23002392
startedAt: recording.startedAt,
23012393
lastWriteAt: recording.lastWriteAt,

0 commit comments

Comments
 (0)