@metagptx/deepflow-suite-cli 0.3.0 → 0.3.2

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.
Files changed (32) hide show
  1. package/bundle/deepflow-release.json +61 -16
  2. package/bundle/skills/df-agentctrl/SKILL.md +9 -87
  3. package/bundle/skills/df-agentctrl/references/environment.md +40 -0
  4. package/bundle/skills/df-agentctrl/references/query-scope.md +50 -0
  5. package/bundle/skills/df-agentrt/SKILL.md +25 -61
  6. package/bundle/skills/df-agentrt/references/environment.md +29 -0
  7. package/bundle/skills/df-agentrt/references/invocation-result.md +11 -0
  8. package/bundle/skills/df-agentrt/references/query-scope.md +30 -0
  9. package/bundle/skills/df-agentrt/references/session-create.md +14 -0
  10. package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
  11. package/bundle/skills/df-task/SKILL.md +30 -92
  12. package/bundle/skills/df-task/references/environment.md +50 -0
  13. package/bundle/skills/df-task/references/fact-types.md +29 -3
  14. package/bundle/skills/df-task/references/query-scope.md +49 -0
  15. package/bundle/skills/df-task/references/reading.md +9 -0
  16. package/bundle/skills/df-taskdriver/SKILL.md +3 -3
  17. package/bundle/skills/df-workbench/SKILL.md +13 -1
  18. package/manifest.json +2 -1
  19. package/package.json +1 -1
  20. package/runtime/df-agentctrl/release.json +1 -1
  21. package/runtime/df-agentrt/release.json +1 -1
  22. package/runtime/df-agentrt/result.mjs +44 -0
  23. package/runtime/df-agentrt/runtime.mjs +36 -4
  24. package/runtime/df-agentrt/usage.mjs +2 -2
  25. package/runtime/df-task/commands.mjs +23 -12
  26. package/runtime/df-task/df-task.mjs +34 -8
  27. package/runtime/df-task/protocol.mjs +1 -1
  28. package/runtime/df-task/read.mjs +127 -0
  29. package/runtime/df-task/release.json +1 -1
  30. package/runtime/df-taskdriver/release.json +1 -1
  31. package/runtime/df-workbench/df-workbench.mjs +274 -14
  32. package/runtime/df-workbench/release.json +1 -1
@@ -2,9 +2,10 @@ export const help = `df-task — DeepFlow Task fact log CLI
2
2
 
3
3
  Usage:
4
4
  df-task ls [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
5
- df-task get <task-id> [--metadata-source <source>] [--metadata-query-key <key>]
6
- df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>]
5
+ df-task get <task-id> [--fields <paths> --json] [--metadata-source <source>] [--metadata-query-key <key>]
6
+ df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>] [--idempotency-key <key>]
7
7
  df-task facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
8
+ df-task facts <task-id> --all --output-file <path> --json [--max-pages <n>] [--max-bytes <n>]
8
9
  df-task append <task-id> --type <type> --file payload.json [--idempotency-key <k>]
9
10
  df-task stream [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
10
11
  df-task title <task-id> --title <text>
@@ -13,7 +14,7 @@ Usage:
13
14
  Scheduling shorthands (each one is a wrapper around append):
14
15
  df-task dispatch <task-id> --agent <name> --instruction <text> [--run <run-id>]
15
16
  df-task wait <task-id> --on <what> --reason <text> [--waiting-run <run-id>]
16
- df-task handoff <task-id> --ask <text> [--assignee <who>]
17
+ df-task handoff <task-id> --ask <text> [--assignee <who>] [--participation ensure_member] [--task-assignee <json>]
17
18
 
18
19
  Global options (before or after the command):
19
20
  --base-url <url> Service URL (or DF_TASK_BASE_URL)
@@ -22,7 +23,7 @@ Global options (before or after the command):
22
23
 
23
24
  Environment:
24
25
  DF_TASK_BASE_URL Service URL, default http://127.0.0.1:8090
25
- DF_TASK_ACTOR_KIND Write identity: human | external | task_run | scheduler_run | driver
26
+ DF_TASK_ACTOR_KIND Write identity: human | bot | external | task_run | scheduler_run | driver
26
27
  DF_TASK_ACTOR_ID Write identity id, e.g. the current run id
27
28
 
28
29
  Facts are append-only. The API has no way to change or remove a fact, so neither does this CLI.
@@ -41,9 +42,10 @@ export const commands = {
41
42
  "writes": false
42
43
  },
43
44
  "get": {
44
- "usage": "get <task-id> [--metadata-source <source>] [--metadata-query-key <key>]",
45
- "description": "Read one task's identity.",
45
+ "usage": "get <task-id> [--fields <paths> --json] [--metadata-source <source>] [--metadata-query-key <key>]",
46
+ "description": "Read one task's identity. --fields selects known fields and object paths; absent paths are omitted.",
46
47
  "flags": [
48
+ "--fields",
47
49
  "--metadata-source",
48
50
  "--metadata-query-key"
49
51
  ],
@@ -52,13 +54,14 @@ export const commands = {
52
54
  "writes": false
53
55
  },
54
56
  "create": {
55
- "usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>]",
57
+ "usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>] [--idempotency-key <key>]",
56
58
  "description": "Create a task, optionally with its first user message in the same transaction.",
57
59
  "flags": [
58
60
  "--title",
59
61
  "--description",
60
62
  "--message",
61
- "--metadata"
63
+ "--metadata",
64
+ "--idempotency-key"
62
65
  ],
63
66
  "required": [
64
67
  "--title"
@@ -67,9 +70,14 @@ export const commands = {
67
70
  "writes": true
68
71
  },
69
72
  "facts": {
70
- "usage": "facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
71
- "description": "Read one task's facts in seq order.",
73
+ "usage": "facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--fields <paths> --json | --all --output-file <path> --json [--max-pages <n>] [--max-bytes <n>]] [--metadata-source <source>] [--metadata-query-key <key>]",
74
+ "description": "Read facts in seq order. --fields selects fields per fact, retaining pagination. --all writes complete raw pages to a private file and prints a manifest; limits default to 10 pages / 8 MiB. Only an observed empty page marks complete. Partial reads exit 1.",
72
75
  "flags": [
76
+ "--fields",
77
+ "--all",
78
+ "--output-file",
79
+ "--max-pages",
80
+ "--max-bytes",
73
81
  "--after",
74
82
  "--types",
75
83
  "--limit",
@@ -166,11 +174,14 @@ export const commands = {
166
174
  "writes": true
167
175
  },
168
176
  "handoff": {
169
- "usage": "handoff <task-id> --ask <text> [--assignee <who>]",
177
+ "usage": "handoff <task-id> --ask <text> [--assignee <who>] [--participation ensure_member] [--task-assignee <json>]",
170
178
  "description": "Append a handoff to a human. Handing the ball back and asking a human to do a step are the same fact.",
171
179
  "flags": [
172
180
  "--ask",
173
- "--assignee"
181
+ "--assignee",
182
+ "--participation",
183
+ "--task-assignee",
184
+ "--idempotency-key"
174
185
  ],
175
186
  "required": [
176
187
  "--ask"
@@ -4,9 +4,10 @@ import { fileURLToPath } from 'node:url';
4
4
  import { realpathSync } from 'node:fs';
5
5
  import { help, commands } from './commands.mjs';
6
6
  import { factTypes, actorKinds } from './protocol.mjs';
7
+ import { validateReadOptions, projectResponse, readAllFacts, readResponse } from './read.mjs';
7
8
 
8
9
  const uuid = /^(?:urn:uuid:)?(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\})$/i;
9
- const valueFlags = new Set(['--base-url', ...Object.values(commands).flatMap((spec) => spec.flags)]);
10
+ const valueFlags = new Set(['--base-url', ...Object.values(commands).flatMap((spec) => spec.flags).filter((flag) => flag !== '--all')]);
10
11
  export function parse(args) {
11
12
  const o = { command: '', args: [], values: {}, json: false, help: false };
12
13
  const seen = new Set();
@@ -20,6 +21,9 @@ export function parse(args) {
20
21
  if (['--help', '-h', '--json'].includes(key)) {
21
22
  if (equals >= 0) throw Error(`${key} does not take a value`);
22
23
  o[key === '--json' ? 'json' : 'help'] = true;
24
+ } else if (key === '--all') {
25
+ if (equals >= 0) throw Error(`${key} does not take a value`);
26
+ o.values[key] = 'true';
23
27
  } else if (valueFlags.has(key)) {
24
28
  if (equals < 0 && (i + 1 === args.length || args[i + 1].startsWith('--'))) throw Error(`${key} requires a value`);
25
29
  o.values[key] = equals < 0 ? args[++i] : arg.slice(equals + 1);
@@ -54,9 +58,23 @@ export function validate(o, env) {
54
58
  }
55
59
  if (spec.writes) {
56
60
  if (!env.DF_TASK_ACTOR_KIND?.trim() || !env.DF_TASK_ACTOR_ID?.trim()) throw Error('writing a fact requires DF_TASK_ACTOR_KIND and DF_TASK_ACTOR_ID');
61
+ if (o.command === 'create') {
62
+ const key = o.values['--idempotency-key'] || '';
63
+ if (key.length > 200 || /[^\x21-\x7e]/.test(key) || (env.DF_TASK_ACTOR_KIND === 'bot' && key.length < 8)) throw Error('creation idempotency key must be at most 200 visible ASCII bytes; bot creation requires at least 8 bytes');
64
+ }
57
65
  if (!actorKinds.includes(env.DF_TASK_ACTOR_KIND)) throw Error(`unknown DF_TASK_ACTOR_KIND ${env.DF_TASK_ACTOR_KIND}`);
58
- if (o.command === 'metadata' && env.DF_TASK_ACTOR_KIND !== 'human') throw Error('metadata requires DF_TASK_ACTOR_KIND=human');
66
+ if (o.command === 'metadata' && !['human','bot'].includes(env.DF_TASK_ACTOR_KIND)) throw Error('metadata requires DF_TASK_ACTOR_KIND=human or bot');
67
+ }
68
+ if (o.command === 'handoff' && ('--participation' in o.values || '--task-assignee' in o.values)) {
69
+ const target = o.values['--assignee'];
70
+ if (o.values['--participation'] !== 'ensure_member') throw Error('--participation must be ensure_member for member operations');
71
+ if (!target || target.trim() !== target || Buffer.byteLength(target) > 200) throw Error('member operations require a non-empty --assignee of at most 200 bytes without surrounding whitespace');
72
+ if ('--task-assignee' in o.values) {
73
+ const assignment = object(o.values['--task-assignee'], '--task-assignee must be a JSON object');
74
+ if (Object.keys(assignment).length !== 2 || assignment.kind !== 'human' || assignment.id !== target) throw Error('--task-assignee must identify the same human as --assignee');
75
+ }
59
76
  }
77
+ validateReadOptions(o);
60
78
  }
61
79
  function object(raw, message) {
62
80
  let value;
@@ -90,8 +108,8 @@ export async function requestFor(o) {
90
108
  let type, payload;
91
109
  if (c === 'dispatch') { type = 'dispatch_intent'; payload = { agent: v['--agent'], instruction: v['--instruction'], mode: v['--run'] ? 'message' : 'new_run', ...(v['--run'] ? { target_run_id: v['--run'] } : {}) }; }
92
110
  if (c === 'wait') { type = 'waiting_reason'; payload = { waiting_on: v['--on'], reason: v['--reason'], ...(v['--waiting-run'] ? { waiting_run_id: v['--waiting-run'] } : {}) }; }
93
- if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}) }; }
94
- return post(`${path}/facts`, { type, payload });
111
+ if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}), ...(v['--participation'] ? { participation: v['--participation'] } : {}), ...(v['--task-assignee'] ? { task_assignee: object(v['--task-assignee'], '--task-assignee must be a JSON object') } : {}) }; }
112
+ return post(`${path}/facts`, { type, payload, ...(v['--idempotency-key'] ? {idempotency_key:v['--idempotency-key']} : {}) });
95
113
  }
96
114
  function human(command, data, stdout) {
97
115
  const page = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value) || {};
@@ -127,7 +145,7 @@ export async function run(args, { env = process.env, stdout = process.stdout, st
127
145
  stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-task ${version}`) + '\n');
128
146
  return 0;
129
147
  }
130
- let o, base, req;
148
+ let o, base, initialRequest;
131
149
  try {
132
150
  o = parse(args);
133
151
  if (o.command === 'help' && !o.args.length) { stdout.write(help); return 0; }
@@ -139,15 +157,23 @@ export async function run(args, { env = process.env, stdout = process.stdout, st
139
157
  base = o.values['--base-url'] || env.DF_TASK_BASE_URL || 'http://127.0.0.1:8090';
140
158
  const url = new URL(base);
141
159
  if (!['http:', 'https:'].includes(url.protocol) || url.search || url.hash || url.username || url.password) throw Error('invalid base URL');
142
- req = await requestFor(o);
160
+ initialRequest = await requestFor(o);
143
161
  } catch (error) { stderr.write(error.message + '\n'); return 2; }
144
- try {
162
+ const request = async (options, limit = 0) => {
163
+ const req = options === o ? initialRequest : await requestFor(options);
145
164
  const headers = req.body ? { 'Content-Type': 'application/json' } : {};
165
+ if (o.command === 'create' && o.values['--idempotency-key']) headers['Idempotency-Key'] = o.values['--idempotency-key'];
146
166
  if (req.method !== 'GET') { headers['X-DF-Actor-Kind'] = env.DF_TASK_ACTOR_KIND; headers['X-DF-Actor-Id'] = env.DF_TASK_ACTOR_ID; }
147
167
  const response = await fetch(base.replace(/\/+$/, '') + req.path, { method: req.method, body: req.body, headers, signal: AbortSignal.timeout(30000) });
148
- const data = await response.text();
168
+ const data = await readResponse(response, limit);
149
169
  if (response.status >= 400) throw Error(`HTTP ${response.status}: ${data.trim()}`);
150
170
  try { JSON.parse(data); } catch { throw Error('server returned invalid JSON'); }
171
+ return data;
172
+ };
173
+ if (o.values['--all']) return readAllFacts(o, base, request, stdout, stderr);
174
+ try {
175
+ let data = await request(o, '--fields' in o.values ? 64 << 20 : 0);
176
+ if ('--fields' in o.values) data = projectResponse(o.command, data, o.values['--fields'].split(','));
151
177
  if (o.json) stdout.write(data + '\n'); else human(o.command, data, stdout);
152
178
  return 0;
153
179
  } catch (error) { stderr.write(error.message + '\n'); return 1; }
@@ -1,3 +1,3 @@
1
1
  // Generated by generate-protocol.mjs from internal/model/fact.go.
2
2
  export const factTypes = ["task_lifecycle","user_message","external_result","artifact_published","summary","help_requested","run_outcome","dispatch_intent","waiting_reason","handoff_to_human","adjudication","run_terminated","invocation_started","invocation_accepted","invocation_ended","attempt_failed"];
3
- export const actorKinds = ["human","external","task_run","scheduler_run","driver"];
3
+ export const actorKinds = ["human","bot","external","task_run","scheduler_run","driver"];
@@ -0,0 +1,127 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, mkdtemp, open, link, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ // Retain every numeric token verbatim, including values outside JS precision.
6
+ export const decodeRaw = (raw) => JSON.parse(raw, (_key, value, context) => typeof value === 'number' ? JSON.rawJSON(context.source) : value);
7
+ export const encodeRaw = (value) => JSON.stringify(value);
8
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && !JSON.isRawJSON(value);
9
+ const seq = (value) => {
10
+ if (!JSON.isRawJSON(value) || !/^-?\d+$/.test(value.rawJSON)) throw Error('invalid facts sequence');
11
+ const n = BigInt(value.rawJSON);
12
+ if (n < 0n || n > 9223372036854775807n) throw Error('invalid facts sequence');
13
+ return n;
14
+ };
15
+ const rawSeq = (n) => JSON.rawJSON(BigInt(n).toString());
16
+
17
+ export function validateReadOptions(o) {
18
+ const v = o.values;
19
+ if ('--fields' in v) {
20
+ if (!o.json) throw Error('--fields requires --json');
21
+ const roots = o.command === 'get' ? ['id', 'title', 'description', 'description_version', 'creator', 'metadata', 'created_at', 'updated_at'] : ['seq', 'type', 'actor_kind', 'actor_id', 'payload', 'idempotency_key', 'created_at'];
22
+ const paths = v['--fields'].split(',');
23
+ paths.forEach((field, i) => {
24
+ const parts = field.split('.');
25
+ if (!roots.includes(parts[0]) || (parts.length > 1 && !['metadata', 'payload'].includes(parts[0]))) throw Error(`unsupported field path ${JSON.stringify(field)} for ${o.command}`);
26
+ if (parts.some((part) => !part || part.trim() !== part || /[\[\]*\/\\\t\r\n]/.test(part))) throw Error(`invalid object field path ${JSON.stringify(field)}`);
27
+ if (paths.slice(0, i).some((earlier) => earlier === field || earlier.startsWith(field + '.') || field.startsWith(earlier + '.'))) throw Error('overlapping field paths');
28
+ });
29
+ }
30
+ const all = '--all' in v;
31
+ if (all && (!o.json || !v['--output-file']?.trim() || '--fields' in v)) throw Error('--all requires --json and --output-file, and cannot be combined with --fields');
32
+ for (const flag of ['--output-file', '--max-pages', '--max-bytes']) if (flag in v && !all) throw Error(`${flag} requires --all`);
33
+ for (const [flag, max] of [['--max-pages', 100], ['--max-bytes', 64 << 20]]) {
34
+ if (flag in v && (!/^\+?\d+$/.test(v[flag]) || BigInt(v[flag]) < 1n || BigInt(v[flag]) > BigInt(max))) throw Error(`${flag} must be between 1 and ${max}`);
35
+ }
36
+ }
37
+
38
+ function projectObject(source, paths) {
39
+ if (!isObject(source)) throw Error('expected a JSON object for field selection');
40
+ const selected = Object.create(null), children = new Map();
41
+ for (const field of paths) {
42
+ const [root, ...parts] = field.split('.');
43
+ if (parts.length) { if (!children.has(root)) children.set(root, []); children.get(root).push(parts.join('.')); }
44
+ else if (Object.hasOwn(source, root)) selected[root] = source[root];
45
+ }
46
+ for (const [root, subpaths] of children) {
47
+ if (!Object.hasOwn(source, root) || !isObject(source[root])) continue;
48
+ const projected = projectObject(source[root], subpaths);
49
+ if (Object.keys(projected).length) selected[root] = projected;
50
+ }
51
+ return selected;
52
+ }
53
+ export function projectResponse(command, raw, paths) {
54
+ const data = decodeRaw(raw);
55
+ if (command === 'get') return encodeRaw(projectObject(data, paths));
56
+ if (!isObject(data) || !Array.isArray(data.facts)) throw Error('expected facts array');
57
+ data.facts = data.facts.map((fact) => projectObject(fact, paths));
58
+ return encodeRaw(data);
59
+ }
60
+ const normalizedID = (id) => typeof id === 'string' ? id.toLowerCase().replace(/^urn:uuid:/, '').replace(/[{}-]/g, '') : '';
61
+ export function validateFactPage(raw, taskID, after) {
62
+ const page = decodeRaw(raw);
63
+ if (!isObject(page) || !Array.isArray(page.facts) || !Object.hasOwn(page, 'next_after_seq')) throw Error('invalid facts page identity, facts or cursor');
64
+ if (!/^[0-9a-f]{32}$/.test(normalizedID(page.task_id)) || normalizedID(page.task_id) !== normalizedID(taskID)) throw Error('facts page task_id does not match query');
65
+ let last = after;
66
+ for (const fact of page.facts) {
67
+ if (!isObject(fact) || typeof fact.type !== 'string' || !fact.type || seq(fact.seq) <= last) throw Error(`facts page is not strictly ordered after ${after}`);
68
+ last = seq(fact.seq);
69
+ }
70
+ if (seq(page.next_after_seq) !== last) throw Error(`facts cursor does not match last seq ${last}`);
71
+ return { facts: page.facts, next: last };
72
+ }
73
+ export async function readResponse(response, limit = 0) {
74
+ if (!limit) return response.text();
75
+ let bytes = 0; const chunks = [];
76
+ for await (const chunk of response.body) {
77
+ bytes += chunk.length;
78
+ if (bytes > limit) throw Object.assign(Error('read byte budget exhausted'), { byteLimit: true });
79
+ chunks.push(chunk);
80
+ }
81
+ return Buffer.concat(chunks).toString('utf8');
82
+ }
83
+
84
+ export async function readAllFacts(o, baseURL, request, stdout, stderr) {
85
+ const output = path.resolve(o.values['--output-file']);
86
+ let temp;
87
+ try {
88
+ try { await lstat(output); throw Error(`output file must not exist: ${output}`); }
89
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
90
+ temp = await mkdtemp(path.join(path.dirname(output), '.task-facts-'));
91
+ } catch (error) { stderr.write(error.message + '\n'); return 2; }
92
+ try {
93
+ const query = { task_id: o.args[0], base_url: baseURL, after_seq: '0', limit: '200' };
94
+ for (const flag of ['--after', '--types', '--limit', '--metadata-source', '--metadata-query-key']) if (flag in o.values) query[flag === '--after' ? 'after_seq' : flag.slice(2).replaceAll('-', '_')] = o.values[flag];
95
+ const manifest = { query, complete: false, stop_reason: 'page_limit', pages: 0, count: 0, next_after_seq: rawSeq(query.after_seq), type_counts: Object.create(null), response_bytes: 0, output_file: output };
96
+ const facts = [], budget = Number(o.values['--max-bytes'] || (8 << 20));
97
+ let cursor = BigInt(query.after_seq);
98
+ for (let i = 0; i < Number(o.values['--max-pages'] || 10); i++) {
99
+ const remaining = budget - manifest.response_bytes;
100
+ if (remaining <= 0) { manifest.stop_reason = 'byte_limit'; break; }
101
+ let raw, page;
102
+ try { raw = await request({ ...o, values: { ...o.values, '--after': String(cursor) } }, remaining); }
103
+ catch (error) { manifest.stop_reason = error.byteLimit ? 'byte_limit' : 'request_error'; stderr.write(error.message + '\n'); break; }
104
+ manifest.response_bytes += Buffer.byteLength(raw);
105
+ try { page = validateFactPage(raw, o.args[0], cursor); }
106
+ catch (error) { manifest.stop_reason = 'invalid_page'; stderr.write(error.message + '\n'); break; }
107
+ manifest.pages++;
108
+ if (!page.facts.length) { manifest.complete = true; manifest.stop_reason = 'empty_page'; break; }
109
+ for (const fact of page.facts) {
110
+ if (!manifest.count) manifest.first_seq = fact.seq;
111
+ manifest.last_seq = fact.seq;
112
+ manifest.type_counts[fact.type] = (manifest.type_counts[fact.type] || 0) + 1;
113
+ manifest.count++;
114
+ }
115
+ facts.push(...page.facts); cursor = page.next; manifest.next_after_seq = rawSeq(cursor);
116
+ }
117
+ const data = Buffer.from(encodeRaw({ query, facts, complete: manifest.complete, stop_reason: manifest.stop_reason, next_after_seq: manifest.next_after_seq }));
118
+ const staging = path.join(temp, 'facts.json'), file = await open(staging, 'wx', 0o600);
119
+ try { await file.writeFile(data); } finally { await file.close(); }
120
+ await link(staging, output);
121
+ manifest.file_bytes = data.length; manifest.file_sha256 = createHash('sha256').update(data).digest('hex');
122
+ stdout.write(encodeRaw(manifest) + '\n');
123
+ if (!manifest.complete) { stderr.write(`incomplete facts read: ${manifest.stop_reason}; next_after_seq=${cursor}\n`); return 1; }
124
+ return 0;
125
+ } catch (error) { stderr.write(error.message + '\n'); return 1; }
126
+ finally { await rm(temp, { recursive: true, force: true }); }
127
+ }
@@ -1 +1 @@
1
- {"version":"0.3.0","project":"deepflow-task","sourceCommit":"3a8f5c8915cf5df74236eb4da693ac75f4c816a8","sourceDirty":false}
1
+ {"version":"0.3.2","project":"deepflow-task","sourceCommit":"cc53e86834b19e8a4fe73f9e9a08d76853d51e51","sourceDirty":false}
@@ -1 +1 @@
1
- {"version":"0.3.0","project":"deepflow-task-driver","sourceCommit":"ab582505c8d60a99c8d2939c3d58b31803a8a229","sourceDirty":false}
1
+ {"version":"0.3.2","project":"deepflow-task-driver","sourceCommit":"c05efdc13dfd0d7a9043007f7421dbd1ac6daab2","sourceDirty":false}